Skip to content

Instantly share code, notes, and snippets.

@tkrajina
Last active January 30, 2024 05:46
Show Gist options
  • Star 44 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save tkrajina/aec8d1b15b088c20f0df4afcd5f0c511 to your computer and use it in GitHub Desktop.
Save tkrajina/aec8d1b15b088c20f0df4afcd5f0c511 to your computer and use it in GitHub Desktop.
Unmarshal JSON to specific interface implementation
package main
import (
"encoding/json"
"fmt"
"reflect"
)
type Something interface{}
type Something1 struct {
Aaa, Bbb string
}
type Something2 struct {
Ccc, Ddd string
}
var _ Something = (*Something1)(nil)
var _ Something = (*Something2)(nil)
type Container struct {
Type string `json:"type"`
Value Something `json:"value"`
}
func (c *Container) UnmarshalJSON(data []byte) error {
value, err := UnmarshalCustomValue(data, "type", "value", map[string]reflect.Type{
"something1": reflect.TypeOf(Something1{}),
"something2": reflect.TypeOf(Something2{}),
})
if err != nil {
return err
}
c.Value = value
return nil
}
func UnmarshalCustomValue(data []byte, typeJsonField, valueJsonField string, customTypes map[string]reflect.Type) (interface{}, error) {
m := map[string]interface{}{}
if err := json.Unmarshal(data, &m); err != nil {
return nil, err
}
typeName := m[typeJsonField].(string)
var value Something
if ty, found := customTypes[typeName]; found {
value = reflect.New(ty).Interface().(Something)
}
valueBytes, err := json.Marshal(m[valueJsonField])
if err != nil {
return nil, err
}
if err = json.Unmarshal(valueBytes, &value); err != nil {
return nil, err
}
return value, nil
}
var _ json.Unmarshaler = (*Container)(nil)
func panicIfErr(err error) {
if err != nil {
panic(err.Error())
}
}
func main() {
testUnmarshalling(`{"type":"something1","value":{"Aaa": "a"}}`)
testUnmarshalling(`{"type":"something2","value":{"Ccc": "a"}}`)
}
func testUnmarshalling(jsonStr string) {
var container Container
err := json.Unmarshal([]byte(jsonStr), &container)
panicIfErr(err)
fmt.Printf("container=%+v\n", container)
fmt.Printf("value=%#v\n", container.Value)
}
@bittermandel
Copy link

Thank you so much for this!

@gohumble
Copy link

thanks that saved me a lot of time

Copy link

ghost commented Nov 15, 2021

Instead of re-marshaling and re-unmarshaling on line 53 you could modify this to first unmarshal to json.RawMessage and then unmarshal that once you've determined the type. Should have a modest performance increase.

type typedEncoding struct {
	Type  string
	Value json.RawMessage
}

@weidonglian
Copy link

Thank you so much for this!

You are welcome!

@hlubek
Copy link

hlubek commented Mar 8, 2023

Thanks! I ended up doing this:

package main

import (
	"encoding/json"
	"reflect"
)

type Something interface{}

type Something1 struct {
	Aaa, Bbb string
}

type Something2 struct {
	Ccc, Ddd string
}

var _ Something = Something1{}
var _ Something = Something2{}

// We need to register all known message types here to be able to unmarshal them to the correct interface type.
var knownImplementations = []Something{
	Something1{},
	Something2{},
}

type Container struct {
	Value Something `json:"value"`
}

func (c *Container) UnmarshalJSON(bytes []byte) error {
	var data struct {
		Type  string
		Value json.RawMessage
	}
	if err := json.Unmarshal(bytes, &data); err != nil {
		return err
	}

	for _, knownImplementation := range knownImplementations {
		knownType := reflect.TypeOf(knownImplementation)
		if knownType.String() == data.Type {
			// Create a new pointer to a value of the concrete message type
			target := reflect.New(knownType)
			// Unmarshal the data to an interface to the concrete value (which will act as a pointer, don't ask why)
			if err := json.Unmarshal(data.Value, target.Interface()); err != nil {
				return err
			}
			// Now we get the element value of the target and convert it to the interface type (this is to get rid of a pointer type instead of a plain struct value)
			c.Value = target.Elem().Interface().(Something)
			return nil
		}
	}

	return nil
}

func (c Container) MarshalJSON() ([]byte, error) {
	// Marshal to type and actual data to handle unmarshaling to specific interface type
	return json.Marshal(struct {
		Type  string
		Value any
	}{
		Type:  reflect.TypeOf(c.Value).String(),
		Value: c.Value,
	})
}

func main() {
	c := Container{
		Value: Something1{
			Aaa: "aaa",
			Bbb: "bbb",
		},
	}

	data, err := json.Marshal(c)
	if err != nil {
		panic(err)
	}

	var unmarshaled Container
	err = json.Unmarshal(data, &unmarshaled)
	if err != nil {
		panic(err)
	}

	switch v := unmarshaled.Value.(type) {
	case Something1:
		println(v.Aaa)
	default:
		panic("unexpected value type: " + reflect.TypeOf(v).String())
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment