Idiomatic way to deserialise JSON to type based on string in Go

Issue

I’m using Go v1.17.3

I’m pretty new with Go and coming from an OOP background I’m very aware I’m not in the Gopher mindset yet! So I’ve split the question in 2 sections, the first is the problem I’m trying to solve, the second is what I’ve done so far. That way if I’ve approached the solution in a really strange way from what idiomatic Go should look like the problem should still be clear.

1. Problem I’m trying to solve:

Deserialise a JSON request to a struct where the struct name is specified in one of the fields on the request.

Example code

Request:

{
    "id": "1",
    "name": "example-document",
    "dtos": [
        {
            "type": "DTOA",
            "attributes": {
                "name": "Geoff"
            }
        },
        {
            "type": "DTOB",
            "attributes": {
                "length": "24cm"
            }
        }
    ]
}

And I want to end up with a collection of interface types.

2. What I’ve done so far

I’ve got a package called dto which models the behviours each DTO is capable of.

package dto

type DTO interface {
    Deserialize(attributes json.RawMessage) error
    ToEntity() (*entity.Entity, error)
}

type RawDTO struct {
    Type       string `json:"type"`
    Attributes json.RawMessage
}

type DTOA {
    Name string `json:"name"`
}

func (dto *DTOA) Deserialize(attributes json.RawMessage) error {
  // Unmarshall json to address of t
}

func (dto *DTOA) ToEntity() (*entity.Entity, error) {
  // Handle creation of EntityA
}

type DTOB {
    Length string `json:"length"`
}

func (dto *DTOB) Deserialize(attributes json.RawMessage) error {
  // Unmarshall json to address of t
}

func (dto *DTOB) ToEntity() (*entity.Entity, error) {
  // Handle creation of EntityB
}

For context, Entity is an interface in another package.

I’ve created a type registry by following the answers suggested from this StackOverflow question

This looks like:

package dto

var typeRegistry = make(map[string]reflect.Type)

func registerType(typedNil interface{}) {
    t := reflect.TypeOf(typedNil).Elem()
    typeRegistry[t.PkgPath()+"."+t.Name()] = t
}

func LoadTypes() {
    registerType((*DTOA)(nil))
    registerType((*DTOB)(nil))
}

func MakeInstance(name string) (DTO, error) {
    if _, ok := typeRegistry[name]; ok {
        return reflect.New(typeRegistry[name]).Elem().Addr().Interface().(DTO), nil
    }

    return nil, fmt.Errorf("[%s] is not a registered type", name)
}

When I bring this all together:

package commands

type CreateCommand struct {
    ID   string       `json:"id"`
    Name string       `json:"name"`
    DTOs []dto.RawDTO `json:"dtos"`
}

func CreateCommandHandler(w http.ResponseWriter, r *http.Request) {
    var cmd CreateCommand
    bodyBytes, err := ioutil.ReadAll(r.Body)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    err = json.Unmarshal(bodyBytes, &cmd)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    var entities []*entity.Entity
    for _, v := range cmd.DTOs {
        // I have a zero instance of a type that implements the DTO interface
        dto, err := dto.MakeInstance("path_to_package." + v.Type)
        if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            return
        }
        // Each registered type implements Deserialize
        err = dto.Deserialize(v.Attributes)
        if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            return
        }
        // Each registered type implements ToEntity
        e, err := dto.ToEntity()
        if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            return
        }
        entities = append(entities, e)
    }

    w.WriteHeader(http.StatusOK)
}

The issue

When I execute this code and send a request, I get the following error:

http: panic serving 127.0.0.1:34020: interface conversion: *dto.DTOA is not dto.DTO: missing method ToEntity
goroutine 18 [running]:

I can’t figure out why this is happening. The Deserialize method works fine.

Solution

func CreateCommandHandler(w http.ResponseWriter, r *http.Request) {
    var cmd CreateCommand
    if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
        w.WriteHeader(http.StatusBadRequest)
        return
    }

    var entities []*entity.Entity
    for _, v := range cmd.DTOs {
        e, err := v.DTO.ToEntity()
        if err != nil {
            w.WriteHeader(http.StatusBadRequest)
            return
        }
        entities = append(entities, e)
    }

    w.WriteHeader(http.StatusOK)
}

Your handler could look like the above if you do the following:

  1. Drop the reflection from the registry.
var typeRegistry = map[string]func() DTO{
    "DTOA": func() DTO { return new(DTOA) },
    "DTOB": func() DTO { return new(DTOB) },
}
  1. Implement a custom json.Unmarshaler.
type DTOUnmarshaler struct {
    DTO DTO
}

func (u *DTOUnmarshaler) UnmarshalJSON(data []byte) error {
    var raw struct {
        Type       string `json:"type"`
        Attributes json.RawMessage
    }
    if err := json.Unmarshal(data, &raw); err != nil {
        return err
    }
    u.DTO = typeRegistry[raw.Type]()
    return json.Unmarshal(raw.Attributes, u.DTO)
}
  1. In the CreateCommand type use the custom unmarshaler instead of the RawDTO type.
type CreateCommand struct {
    ID   string               `json:"id"`
    Name string               `json:"name"`
    DTOs []dto.DTOUnmarshaler `json:"dtos"`
}

Done.


Bonus: you get to simplify your DTOs since you don’t need Deserialize anymore.

type DTO interface {
    ToEntity() (*entity.Entity, error)
}

type DTOA struct {
    Name string `json:"name"`
}

func (dto *DTOA) ToEntity() (*entity.Entity, error) {
    // Handle creation of EntityA
}

type DTOB struct {
    Length string `json:"length"`
}

func (dto *DTOB) ToEntity() (*entity.Entity, error) {
    // Handle creation of EntityB
}

Answered By – mkopriva

Answer Checked By – Willingham (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.