3

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.

2
  • What happens if you write _ = new(DTOA).(DTO)? Does it compile? Commented Feb 2, 2022 at 12:46
  • 1
    @Dani: that can't compile because you cannot type assert a pointer to an interface. Use a type conversion. Commented Feb 2, 2022 at 13:22

1 Answer 1

4
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
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.