Skip to content

CRSylar/jsonpartial

Repository files navigation

jsonpartial

A Go library that extends encoding/json with directional field filtering using struct tags.

Problem

When building web APIs in Go, you often encounter a common problem: some struct fields should only be present in certain directions (request or response), but not both.

Example Scenario:

type Todo struct {
    ID          string `json:"id"`          // Generated by server, not needed in requests
    Name        string `json:"name"`        // Both request and response
    Status      string `json:"status"`      // Determined by server logic
    Description string `json:"description"` // Only needed in requests (just an example)
}

Common Workarounds:

  1. Create duplicate structs (e.g., TodoRequest, TodoResponse)
  2. Manually zero out fields in handler code
  3. Use complex custom marshaler interfaces

All these approaches add boilerplate and reduce maintainability.

Solution

jsonpartial provides a clean, tag-based solution that works as a drop-in replacement for encoding/json:

type Todo struct {
    ID          string `json:"id,marshalonly"`          // Only in responses
    Name        string `json:"name"`                      // Both directions
    Status      string `json:"status,marshalonly"`      // Only in responses  
    Description string `json:"description,unmarshalonly"` // Only in requests
}

Installation

go get github.com/CRSylar/jsonpartial

Usage

Basic Usage

package main

import (
    "fmt"
    "github.com/CRSylar/jsonpartial"
)

type User struct {
    ID        string `json:"id,marshalonly"`       // Server-generated
    Email     string `json:"email"`                // Both directions
    Password  string `json:"password,unmarshalonly"` // Never return in response
}

func main() {
    // POST /users request body
    request := []byte(`{
        "id": "should-be-ignored",
        "email": "user@example.com",
        "password": "secret123"
    }`)
    
    var user User
    if err := jsonpartial.Unmarshal(request, &user); err != nil {
        panic(err)
    }
    
    fmt.Printf("ID: %q (empty because marshalonly)\n", user.ID)
    fmt.Printf("Email: %q\n", user.Email)
    fmt.Printf("Password: %q\n", user.Password)
    
    // GET /users response
    data, _ := jsonpartial.Marshal(user)
    fmt.Printf("Response: %s\n", string(data))
    // Output: {"id":"","email":"user@example.com"}
    // Note: password is excluded because it's unmarshalonly
}

HTTP Handler Example

func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    
    // Automatically filters out marshalonly fields (like ID) from request
    if err := jsonpartial.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    // Save to database, generate ID...
    user.ID = generateID()
    
    // Automatically filters out unmarshalonly fields (like Password) from response
    w.Header().Set("Content-Type", "application/json")
    jsonpartial.NewEncoder(w).Encode(user)
}

Supported Tags

Tag Behavior
json:"field,marshalonly" Field only serialized (output), ignored during deserialization
json:"field,unmarshalonly" Field only deserialized (input), excluded from serialization
json:"field,marshalonly,unmarshalonly" Both tags cancel each other out - normal behavior
json:"field,-" Completely ignored in both directions

Embedded Structs

Tags are inherited by embedded struct fields:

type Base struct {
    ID string `json:"id,marshalonly"`
}

type User struct {
    Base
    Name string `json:"name"`
}

// ID will be marshalonly in User as well

Streaming API

For large JSON or HTTP handlers:

// Reading from request
func handler(w http.ResponseWriter, r *http.Request) {
    var data MyStruct
    decoder := jsonpartial.NewDecoder(r.Body)
    decoder.Decode(&data)
    // data now has unmarshalonly fields populated, marshalonly fields empty
}

// Writing to response
func handler(w http.ResponseWriter, r *http.Request) {
    data := MyStruct{...}
    encoder := jsonpartial.NewEncoder(w)
    encoder.Encode(data)
    // Response excludes unmarshalonly fields
}

Formatting Options

// Compact output (default)
encoder := jsonpartial.NewEncoder(w)

// Indented output
encoder := jsonpartial.NewEncoder(w)
encoder.SetIndent("", "  ")

// Indented with prefix
encoder.SetIndent("prefix:", "  ")

API Reference

Functions

  • Marshal(v any) ([]byte, error) - Serialize with filtering
  • MarshalIndent(v any, prefix, indent string) ([]byte, error) - Serialize with indentation
  • Unmarshal(data []byte, v any) error - Deserialize with filtering

Types

  • Encoder - Streaming encoder with SetIndent, SetEscapeHTML methods
  • Decoder - Streaming decoder with Token, More, UseNumber methods

Testing

go test ./...

All 58 tests passing.

Limitations

  • v0.1.0: Basic struct, slice, map, and pointer support
  • Conflicting tags (marshalonly/unmarshalonly + omitempty/omitzero) will error
  • Circular references not protected (deferred to v0.2.0)
  • Missing caching stategies to avoid recomputation in reflection (deferred to v0.2.0+)

Why is my IDE warning about "unknown json tag option"?

gopls (the Go language server) maintains a hardcoded list of valid json tag options (omitempty, string, etc.) and doesn't recognize marshalonly/unmarshalonly. These warnings are false positives - your code is correct and will work as expected.

Quick fixes:

  • VS Code: Add "go.diagnostic.vulncheck": "Off" to settings
  • Neovim: Disable gopls struct tag analysis in your LSP config
  • GoLand: Disable "Unknown JSON tag options" inspection in Settings → Editor → Inspections

License

MIT License - See LICENSE file for details

Contributing

Contributions welcome! Please open an issue or PR.


Built with ❤️ for cleaner Go APIs

About

A Go library that extends `encoding/json` with directional field filtering using struct tags.

Resources

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages