A Go package for unmarshaling JSON with discriminated unions (tagged unions) using type-safe constant values.
This package leverages the new experimental encoding/json/v2 API to provide compile-time type safety when working with polymorphic JSON data. Currently to avoid the requirement to set GOEXPERIMENT, this depends on https://github.com/go-json-experiment/json rather than encoding/json/v2 itself.
Use this package when you need to unmarshal JSON objects where a discriminator field determines the concrete type:
- API responses with a
typeorkindfield that varies the structure - Event systems where different events have different payloads
- Message queues with polymorphic message types
- Configuration files with variant sections
- Any JSON that uses tagged unions or discriminated unions
- Any JSON where a field is of a known constant value.
import (
"github.com/cue-exp/jsondiscrim"
"github.com/go-json-experiment/json"
)
// Define an interface for your message types
type Message interface {
Format() string
}
// BaseMessage should be embedded by any [Message] implementation
// to define its discriminator field. The S type parameter should
// be of the form expected by [jsondiscrim.Const].
type BaseMessage[S any] struct {
Type jsondiscrim.Const[string, S] `json:"type"`
}
// Define concrete types with a Const discriminator field
type TextMessage struct {
BaseMessage[struct {
string `const:"text"`
}]
Text string `json:"text"`
}
type ImageMessage struct {
BaseMessage[struct {
string `const:"image"`
}]
URL string `json:"url"`
Alt string `json:"alt"`
}
// Create an unmarshaler for the interface
unmarshalers := jsondiscrim.Structs[Message](
(*TextMessage)(nil),
(*ImageMessage)(nil),
)
// Unmarshal JSON that will be dispatched to the correct type
conversationJSON := `{
"messages": [
{"type":"text","text":"Hello!"},
{"type":"image","url":"pic.jpg","alt":"A picture"}
]
}`
var conv Conversation
err := json.Unmarshal(
[]byte(conversationJSON),
&conv,
json.WithUnmarshalers(unmarshalers),
)The package uses the following pattern to encode constant values as types:
Const[string, struct{ string `const:"foo"` }]This reads as: "a constant of type string with the value "foo"".
-
Const[T, S]- A generic type with two parameters:T- the type of the constant (string, int, bool, etc.)S- a struct that encodes the actual constant value
-
struct{ stringconst:"foo"}- An anonymous struct with:- One field of type
T(here,string) - A struct tag
const:"foo"containing the constant's value
- One field of type
-
Base types for convenience:
type BaseMessage[S any] struct { Type Const[string, S] `json:"type"` }]
- Represents the general discriminator pattern for a particular interface type
- Designed to be embedded
This idiom provides:
- Compile-time type safety - each constant value is a distinct type
- Low runtime overhead - discriminator values are zero size and computed once via reflection
- Automatic marshaling -
Constfields always marshal to their constant value - Validation on unmarshal - unmarshaling fails if the value doesn't match
- Define your types - Each concrete type has a
Constfield with a unique value - Call
Structs- Pass pointers to zero values of each concrete type - Automatic detection -
Structsexamines the types to find the discriminator field - Type-safe unmarshaling - JSON is dispatched to the correct concrete type based on the discriminator
The Structs function:
- Finds the common field across all types that has different
Constvalues - Creates an unmarshaler that reads the discriminator field from JSON
- Dispatches to the appropriate concrete type based on the value
This package currently uses the experimental github.com/go-json-experiment/json package. When Go's encoding/json/v2 moves out of experimental mode and into the standard library, this package will be updated to use the stdlib version.
See LICENSE file.