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

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.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source