// Package jsontypes supports decoding for interface types whose concrete // implementations need to be stored as JSON. To do this, concrete values are // packaged in wrapper objects having the form: // // { // "type": "", // "value": // } // // This package provides a registry for type tag strings and functions to // encode and decode wrapper objects. package jsontypes import ( "bytes" "encoding/json" "fmt" "reflect" ) // The Tagged interface must be implemented by a type in order to register it // with the jsontypes package. The TypeTag method returns a string label that // is used to distinguish objects of that type. type Tagged interface { TypeTag() string } // registry records the mapping from type tags to value types. Values in this // map must be normalized to non-pointer types. var registry = struct { types map[string]reflect.Type }{types: make(map[string]reflect.Type)} // register adds v to the type registry. It reports an error if the tag // returned by v is already registered. func register(v Tagged) error { tag := v.TypeTag() if t, ok := registry.types[tag]; ok { return fmt.Errorf("type tag %q already registered to %v", tag, t) } typ := reflect.TypeOf(v) if typ.Kind() == reflect.Ptr { typ = typ.Elem() } registry.types[tag] = typ return nil } // MustRegister adds v to the type registry. It will panic if the tag returned // by v is already registered. This function is meant for use during program // initialization. func MustRegister(v Tagged) { if err := register(v); err != nil { panic(err) } } type wrapper struct { Type string `json:"type"` Value json.RawMessage `json:"value"` } // Marshal marshals a JSON wrapper object containing v. If v == nil, Marshal // returns the JSON "null" value without error. func Marshal(v Tagged) ([]byte, error) { if v == nil { return []byte("null"), nil } data, err := json.Marshal(v) if err != nil { return nil, err } return json.Marshal(wrapper{ Type: v.TypeTag(), Value: data, }) } // Unmarshal unmarshals a JSON wrapper object into v. It reports an error if // the data do not encode a valid wrapper object, if the wrapper's type tag is // not registered with jsontypes, or if the resulting value is not compatible // with the type of v. func Unmarshal(data []byte, v interface{}) error { // Verify that the target is some kind of pointer. target := reflect.ValueOf(v) if target.Kind() != reflect.Ptr { return fmt.Errorf("target %T is not a pointer", v) } var w wrapper dec := json.NewDecoder(bytes.NewReader(data)) dec.DisallowUnknownFields() if err := dec.Decode(&w); err != nil { return fmt.Errorf("invalid type wrapper: %w", err) } typ, ok := registry.types[w.Type] if !ok { return fmt.Errorf("unknown type tag: %q", w.Type) } else if !typ.AssignableTo(target.Elem().Type()) { return fmt.Errorf("type %v not assignable to %T", typ, v) } obj := reflect.New(typ) if err := json.Unmarshal(w.Value, obj.Interface()); err != nil { return fmt.Errorf("decoding wrapped value: %w", err) } target.Elem().Set(obj.Elem()) return nil }