A Go library that extends encoding/json with directional field filtering using struct tags.
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:
- Create duplicate structs (e.g.,
TodoRequest,TodoResponse) - Manually zero out fields in handler code
- Use complex custom marshaler interfaces
All these approaches add boilerplate and reduce maintainability.
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
}go get github.com/CRSylar/jsonpartialpackage 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
}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)
}| 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 |
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 wellFor 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
}// Compact output (default)
encoder := jsonpartial.NewEncoder(w)
// Indented output
encoder := jsonpartial.NewEncoder(w)
encoder.SetIndent("", " ")
// Indented with prefix
encoder.SetIndent("prefix:", " ")Marshal(v any) ([]byte, error)- Serialize with filteringMarshalIndent(v any, prefix, indent string) ([]byte, error)- Serialize with indentationUnmarshal(data []byte, v any) error- Deserialize with filtering
Encoder- Streaming encoder with SetIndent, SetEscapeHTML methodsDecoder- Streaming decoder with Token, More, UseNumber methods
go test ./...All 58 tests passing.
- 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+)
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
goplsstruct tag analysis in your LSP config - GoLand: Disable "Unknown JSON tag options" inspection in Settings → Editor → Inspections
MIT License - See LICENSE file for details
Contributions welcome! Please open an issue or PR.
Built with ❤️ for cleaner Go APIs