Documentation ¶
Overview ¶
Package oneof enables marshaling and unmarshaling of Go interface values using the Go JSON V2 experiment (github.com/go-json-experiment/json).
By default, marshaling and unmarshaling an interface value will fail:
var s1 fmt.Stringer = crypto.SHA256 b, _ := json.Marshal(s1) // b == []byte("5") var s2 fmt.Stringer err := json.Unmarshal(b, &s2) fmt.Println(err) // Output: // json: cannot unmarshal JSON string into Go value of type fmt.Stringer: cannot derive concrete type for non-empty interface
MarshalFunc and UnmarshalFunc encode matching Go values alongside a type discriminator, which enables round-trip marshaling and unmarshaling:
// Implementations of fmt.Stringer that our program will marshal and unmarshal, // keyed by an option type opts := map[string]fmt.Stringer{ "crypto.Hash": crypto.Hash(0), "net.IP": net.IP{}, "url.URL": &url.URL{}, } marshalFunc := oneof.MarshalFunc(opts, nil) var s1 fmt.Stringer = crypto.SHA256 b, _ := json.Marshal(s1, json.WithMarshalers(marshalFunc)) // b == []byte(`{"_type": "crypto.Hash", "_value": 5}`) unmarshalFunc := oneof.UnmarshalFunc(opts, nil) var s2 fmt.Stringer _ = json.Unmarshal(b, &s2, json.WithUnmarshalers(unmarshalFunc)) fmt.Printf("unmarshaled type = %T\n", s2) fmt.Printf("string output = %s\n", s2.String()) // Output: // unmarshaled type: crypto.Hash // string output: SHA-256
Default encoding ¶
By default, MarshalFunc encodes Go values into a JSON object where:
- The value of the key "_type" is the type discriminator, and,
- The value of the key "_value" is the default JSON-encoding of the Go value
For example, given options:
opts := map[string]fmt.Stringer{ "crypto.Hash": crypto.Hash(0), "net.IP": net.IP{}, "url.URL": &url.URL{}, }
... the Go value crypto.SHA256, which encodes to the JSON number 5, would be encoded by MarshalFunc as:
{ "_type": "crypto.Hash", "_value": 5 }
... and the Go value &url.URL{Scheme: "https", Host: "example.com"}, which encodes to a JSON object, would be encoded by MarshalFunc as:
{ "_type": "url.URL", "_value": { "Scheme": "https", "Host": "example.com" // other url.URL fields omitted } }
Custom encoding ¶
WrappedValue is the interface implemented by containers which can marshal and unmarshal Go types including type information. Configure the WrappedValue used by MarshalFunc and UnmarshalFunc by providing a non-nil WrapFunc to Config:
cfg := oneof.Config{ WrapFunc: oneof.WrapAlwaysNest, } marshalFunc := oneof.MarshalFunc(opts, cfg)
If WrapFunc is nil, MarshalFunc and UnmarshalFunc default to WrapNested, which wraps encoded values under the "_value" key.
oneof also defines WrapInline, which inlines the fields of JSON object values, e.g.,:
{ "_type": "url.URL", "Scheme": "https", "Host": "example.com" // other url.URL fields omitted }
For finer-grained control, you can create your own implementation of WrappedValue, or use CustomValueWrapper (CustomValueWrapper.Wrap can be used as the WrapFunc in Config)
See the Config and CustomValueWrapper examples for more details.
Handling missing keys ¶
If oneof encounters a Go type for which there is no matching option key while marshaling, it will return an error.
You can override this behavior by setting the ReplaceMissingTypeFunc field of Config:
cfg := &oneof.Config{ ReplaceMissingTypeFunc: func(v any) string { return fmt.Sprintf("MISSING_%T", v) }, }
With the above Config, MarshalFunc will use the string produced by ReplaceMissingTypeFunc as the option type for missing values, like:
{ "_type": "MISSING_*crypto.Hash", "_value": 5 },
Note that UnmarshalFunc will likely fail to unmarshal output produced by ReplaceMissingTypeFunc. If you need to marshal and unmarshal a Go type, include it in the option set.
Example ¶
package main import ( "fmt" "io" "strings" "github.com/dhoelle/oneof" "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" ) // // Some examples that implement the fmt.Stringer interface // type LiteralStringer string func (s LiteralStringer) String() string { return string(s) } type JoinStringer struct { A fmt.Stringer `json:"a,omitempty"` B fmt.Stringer `json:"b,omitempty"` Separator string `json:"separator,omitempty"` } func (s JoinStringer) String() string { return fmt.Sprintf("%s%s%s", s.A.String(), s.Separator, s.B.String()) } type ExclamationPointsStringer int func (s ExclamationPointsStringer) String() string { return strings.Repeat("!", int(s)) } // // An example that implements the error interface // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418 type TeapotError struct { ShowCode bool `json:"show_code,omitempty"` } func (e TeapotError) Error() string { if e.ShowCode { return "418 I'm a teapot" } return "I'm a teapot" } // An example struct with interface fields. // We'll marshal and unmarshal this from JSON in the Example function, below. type StringerAndError struct { Stringer fmt.Stringer `json:"stringer,omitempty"` Error error `json:"error,omitempty"` } func main() { // Sets of options which implement the fmt.Stringer // and error interfaces, respectively, keyed by // their discriminator values stringerOptions := map[string]fmt.Stringer{ "literal": LiteralStringer(""), "join": JoinStringer{}, "exclamation": ExclamationPointsStringer(0), } errorOptions := map[string]error{ "no_progress": io.ErrNoProgress, "teapot": TeapotError{}, } // Use oneof.MarshalFunc to create json.Marshalers that // will intercept the marshaling behavior for errors // and fmt.Stringers. // // Note: as of https://github.com/go-json-experiment/json/commits/2e55bd4e08b08427ba10066e9617338e1f113c53/, // the json v2 experiment library will redirect marshaling // behavior to marshal funcs that are defined on interface // types for both: // // a) interface values // (e.g., `var err error = TeapotError{}`), and, // b) types which satisfy an interface // (e.g., `err := TeapotError{}`, where type // TeapotError satisfies interface error`) // errorMarshalFunc := oneof.MarshalFunc(errorOptions, nil) stringerMarshalFunc := oneof.MarshalFunc(stringerOptions, nil) // Combine our marshal funcs into a single *json.Marshalers marshalers := json.NewMarshalers( errorMarshalFunc, stringerMarshalFunc, ) // Add other JSON options as desired marshalOpts := []json.Options{ json.WithMarshalers(marshalers), jsontext.WithIndent(" "), // make the example output easy to read json.Deterministic(true), // make the example output deterministic } in := StringerAndError{ Error: TeapotError{ShowCode: true}, Stringer: JoinStringer{ A: JoinStringer{ A: LiteralStringer("Hello"), Separator: " ", B: LiteralStringer("world"), }, Separator: "", B: ExclamationPointsStringer(2), }, } // Marshal to JSON b, err := json.Marshal(in, marshalOpts...) if err != nil { panic("failed to marshal: " + err.Error()) } // Build unmarshal funcs, similar to the process above errorUnmarshalFunc := oneof.UnmarshalFunc(errorOptions, nil) stringerUnmarshalFunc := oneof.UnmarshalFunc(stringerOptions, nil) unmarshalers := json.NewUnmarshalers( errorUnmarshalFunc, stringerUnmarshalFunc, ) unmarshalOpts := []json.Options{ json.WithUnmarshalers(unmarshalers), } // Unmarshal our JSON into a new, empty StringerAndError out := StringerAndError{} if err := json.Unmarshal(b, &out, unmarshalOpts...); err != nil { panic("failed to unmarshal: " + err.Error()) } fmt.Printf("Marshaled JSON:\n") fmt.Printf("%s\n", string(b)) fmt.Printf("\n") fmt.Printf("Output from unmarshaled Go values:\n") fmt.Printf(" error: %s\n", out.Error.Error()) fmt.Printf(" string: %s\n", out.Stringer.String()) }
Output: Marshaled JSON: { "stringer": { "_type": "join", "_value": { "a": { "_type": "join", "_value": { "a": { "_type": "literal", "_value": "Hello" }, "b": { "_type": "literal", "_value": "world" }, "separator": " " } }, "b": { "_type": "exclamation", "_value": 2 } } }, "error": { "_type": "teapot", "_value": { "show_code": true } } } Output from unmarshaled Go values: error: 418 I'm a teapot string: Hello world!!
Example (SimpleRoundTrip) ¶
package main import ( "crypto" "fmt" "net" "net/url" "github.com/dhoelle/oneof" "github.com/go-json-experiment/json" ) func main() { // Implementations of fmt.Stringer that our program will marshal and // unmarshal, keyed by an option type opts := map[string]fmt.Stringer{ "crypto.Hash": crypto.Hash(0), "net.IP": net.IP{}, "url.URL": &url.URL{}, } marshalFunc := oneof.MarshalFunc(opts, nil) var s1 fmt.Stringer = crypto.SHA256 b, _ := json.Marshal(s1, json.WithMarshalers(marshalFunc)) // b == []byte(`{"_type": "crypto.Hash", "_value": 5}`) unmarshalFunc := oneof.UnmarshalFunc(opts, nil) var s2 fmt.Stringer _ = json.Unmarshal(b, &s2, json.WithUnmarshalers(unmarshalFunc)) fmt.Printf("unmarshaled type = %T\n", s2) fmt.Printf("string output = %s\n", s2.String()) }
Output: unmarshaled type = crypto.Hash string output = SHA-256
Index ¶
- func JSONOptions[T any](opts map[string]T, cfg *Config) json.Options
- func MarshalFunc[T any](opts map[string]T, cfg *Config) *json.Marshalers
- func UnmarshalFunc[T any](opts map[string]T, cfg *Config) *json.Unmarshalers
- type Config
- type CustomValueWrapper
- type ErrUnknownDiscriminatorValue
- type ErrUnknownGoType
- type WrappedValue
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func MarshalFunc ¶
func MarshalFunc[T any](opts map[string]T, cfg *Config) *json.Marshalers
MarshalFunc creates a json.MarshalFuncV2 which can intercept marshaling behavior for values of type T and encode a JSON value that can be unmarshaled by UnmarshalFunc into a Go value of the original type T.
By default, MarshalFunc encodes Go values into a JSON object where:
- The value of the key "_type" is a type discriminator uniquely identifying the initial type T, and,
- The value of the key "_value" is the default JSON-encoding of the Go value
Encoding behavior can be customized by providing a non-nil Config.
func UnmarshalFunc ¶
func UnmarshalFunc[T any](opts map[string]T, cfg *Config) *json.Unmarshalers
UnmarshalFunc creates a json.UnmarshalFuncV2 which will intercept unmarshaling behavior for values of type T.
UnmarshalFunc finds the destination Go type in its option set that matches the JSON type discriminator, then decodes the remaining JSON according to the default JSON encoding of T.
Types ¶
type Config ¶
type Config struct { // By default, if [MarshalFunc] attempts to encode a Go type that is not in // the defined set of options, it returns an error. // // If ReplaceMissingTypeFunc is defined, oneof.MarshalFunc will instead call // ReplaceMissingTypeFunc with the unknown Go value and use the result as the // value for the JSON discriminator. ReplaceMissingTypeFunc func(any) string // WrapFunc wraps a type string and [jsontext.Value] in a Go type that can be // encoded to and decoded from JSON. // // If unset, defaults to [WrapNestObjects]. WrapFunc func(typ string, v jsontext.Value) WrappedValue }
Example (ReplaceMissingTypeFunc) ¶
// Options implementing fmt.Stringer. // (intentionally omitting LiteralStringer) stringerOptions := map[string]fmt.Stringer{ // "literal": LiteralStringer(""), "join": JoinStringer{}, "exclamation": ExclamationPointsStringer(0), } // When oneof.MarshalFunc encounters a Go type that is not // in its list of options, have it generate a discriminator. // // (Note: attempting to json.Marshal the output JSON back // into a Go type will fail) cfg := &oneof.Config{ ReplaceMissingTypeFunc: func(v any) string { return fmt.Sprintf("MISSING_%T", v) }, } stringerMarshalFunc := oneof.MarshalFunc(stringerOptions, cfg) // Add other JSON options as desired marshalOpts := []json.Options{ json.WithMarshalers(stringerMarshalFunc), jsontext.WithIndent(" "), // make the example output easy to read json.Deterministic(true), // make the example output deterministic } in := JoinStringer{ A: JoinStringer{ A: LiteralStringer("Hello"), Separator: " ", B: LiteralStringer("world"), }, Separator: "", B: ExclamationPointsStringer(2), } // Marshal to JSON b, err := json.Marshal(in, marshalOpts...) if err != nil { panic("failed to marshal: " + err.Error()) } fmt.Printf("Marshaled JSON:\n%s\n", string(b))
Output: Marshaled JSON: { "_type": "join", "_value": { "a": { "_type": "join", "_value": { "a": { "_type": "MISSING_*oneof_test.LiteralStringer", "_value": "Hello" }, "b": { "_type": "MISSING_*oneof_test.LiteralStringer", "_value": "world" }, "separator": " " } }, "b": { "_type": "exclamation", "_value": 2 } } }
Example (WrapFunc) ¶
package main import ( "fmt" "github.com/dhoelle/oneof" "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" ) // simpleWrappedValue uses "type" and "value" as the keys for type // discriminators and nested values, respectively. It also inlines objects. // // Note: if option types have fields that conflict with the type and value keys // of the type wrapper (in this example: "type" or "value"), json.Marshal will // not encode either field. Avoid using type and value keys which conflict with // JSON field names used by your data types. type simpleWrappedValue struct { Typ string `json:"type"` NestedValue jsontext.Value `json:"value,omitempty"` InlineValue jsontext.Value `json:",inline"` } func (w simpleWrappedValue) Type() string { return w.Typ } func (w simpleWrappedValue) Value() jsontext.Value { if len(w.NestedValue) > 0 { return w.NestedValue } return w.InlineValue } func wrapSimple(typ string, v jsontext.Value) oneof.WrappedValue { if v.Kind() == '{' { // inline objects return simpleWrappedValue{ Typ: typ, InlineValue: v, } } // nest non-objects return simpleWrappedValue{ Typ: typ, NestedValue: v, } } func main() { stringerOptions := map[string]fmt.Stringer{ "literal": LiteralStringer(""), "join": JoinStringer{}, "exclamation": ExclamationPointsStringer(0), } cfg := &oneof.Config{ WrapFunc: wrapSimple, } stringerMarshalFunc := oneof.MarshalFunc(stringerOptions, cfg) marshalOpts := []json.Options{ json.WithMarshalers(stringerMarshalFunc), jsontext.WithIndent(" "), // make the example output easy to read json.Deterministic(true), // make the example output deterministic } in := JoinStringer{ A: JoinStringer{ A: LiteralStringer("Hello"), Separator: " ", B: LiteralStringer("world"), }, Separator: "", B: ExclamationPointsStringer(2), } // Marshal to JSON b, err := json.Marshal(in, marshalOpts...) if err != nil { panic("failed to marshal: " + err.Error()) } fmt.Printf("Marshaled JSON:\n%s\n", string(b)) }
Output: Marshaled JSON: { "type": "join", "a": { "type": "join", "a": { "type": "literal", "value": "Hello" }, "b": { "type": "literal", "value": "world" }, "separator": " " }, "b": { "type": "exclamation", "value": 2 } }
type CustomValueWrapper ¶
CustomValueWrapper can be used to quickly build a custom WrapFunc.
Note: CustomValueWrapper does additional work at runtime to dynamically encode and decode JSON. In many cases, this will add additional operations over standard value wrappers, including additional round trips of json.Marshal + json.Unmarshal, and additional iterations over the input value.
If performance is a concern, consider building a custom WrappedValue and [WrapFunc]. See Config for an example (WrapFunc).
Example ¶
stringerOptions := map[string]fmt.Stringer{ "literal": LiteralStringer(""), "join": JoinStringer{}, "exclamation": ExclamationPointsStringer(0), } cw := oneof.CustomValueWrapper{ DiscriminatorKey: "$type", NestedValueKey: "$value", InlineObjects: true, } cfg := &oneof.Config{ WrapFunc: cw.Wrap, } stringerMarshalFunc := oneof.MarshalFunc(stringerOptions, cfg) marshalOpts := []json.Options{ json.WithMarshalers(stringerMarshalFunc), jsontext.WithIndent(" "), // make the example output easy to read json.Deterministic(true), // make the example output deterministic } in := JoinStringer{ A: JoinStringer{ A: LiteralStringer("Hello"), Separator: " ", B: LiteralStringer("world"), }, Separator: "", B: ExclamationPointsStringer(2), } // Marshal to JSON b, err := json.Marshal(in, marshalOpts...) if err != nil { panic("failed to marshal: " + err.Error()) } fmt.Printf("Marshaled JSON:\n%s\n", string(b))
Output: Marshaled JSON: { "$type": "join", "a": { "$type": "join", "a": { "$type": "literal", "$value": "Hello" }, "b": { "$type": "literal", "$value": "world" }, "separator": " " }, "b": { "$type": "exclamation", "$value": 2 } }
func (CustomValueWrapper) Empty ¶
func (b CustomValueWrapper) Empty() WrappedValue
func (CustomValueWrapper) Wrap ¶
func (b CustomValueWrapper) Wrap(typ string, v jsontext.Value) WrappedValue
type ErrUnknownDiscriminatorValue ¶
type ErrUnknownDiscriminatorValue struct {
// contains filtered or unexported fields
}
ErrUnknownDiscriminatorValue is the error returned by UnmarshalFunc when it encounters a JSON discriminator value which is not in the provided set of options
func (ErrUnknownDiscriminatorValue) Error ¶
func (e ErrUnknownDiscriminatorValue) Error() string
type ErrUnknownGoType ¶
type ErrUnknownGoType struct {
// contains filtered or unexported fields
}
ErrUnknownGoType is the error returned by MarshalFunc when it encounters a Go type that is not in the provided set of options
func (ErrUnknownGoType) Error ¶
func (e ErrUnknownGoType) Error() string
type WrappedValue ¶
WrappedValue is the interface implemented by types that can encode a Go type and oneof option string into JSON, and can decode that JSON back into a matching Go type.
func WrapInline ¶
func WrapInline(typ string, v jsontext.Value) WrappedValue
WrapInline wraps a oneof value with default "inline" behavior, specifically:
- Type is stored under the "_type" key
- JSON object values are inlined into the same object as the "_type" key
- All non-object JSON values are nested under the "_value" key
The marshaled JSON output looks like:
{ "my_stringers": [ { "_type": "crypto.Hash", "_value": 5 }, { "_type": "url.URL", "Scheme": "https", "Host": "example.com" }, ] }
func WrapNested ¶
func WrapNested(typ string, v jsontext.Value) WrappedValue
WrapNested nests the provided jsontext.Value underneath the "_value" key within the wrapper object