Karthik Venkateswaran
Karthik Venkateswaran

Reputation: 416

Unmarshal json request body to a structure which has a struct member of custom interface type

Lets consider the following code

type A struct {
    Column1 string `json:"column1"`
    Entity CustomInterface `json:"entity"`
}

type CustomInterface interface {
    GetType() string
}

type Entity1 struct {
    ColumnX string `json:"columnx"`
    ColumnY string `json:"columny"`
}

type Entity2 struct {
    ColumnP string `json:"columnp"`
    ColumnQ string `json:"columnq"`
}

func (*e Entity1) GetType() string {
    return "ENTITY1"
}

func (*e Entity2) GetType() string {
    return "ENTITY2"
}

Now if I am trying to bind an instance of A type as follows

var bodyJSON A
ShouldBindWith(&bodyJson, binding.JSON)

I am getting the following error

json: cannot unmarshal object into Go struct field A.entity of type package.CustomInterface

Am I doing anything very silly here?

PS: I just started exploring go. Apologies if this question is very noob level.

Upvotes: 0

Views: 2051

Answers (1)

Nathan
Nathan

Reputation: 56

The json.Unmarshal function by itself does not let you unmarshal into interface types except for the empty interfaces (interface{}) that don't have any methods:

To unmarshal JSON into an interface value, Unmarshal stores one of these in the interface value:

  • bool, for JSON booleans
  • float64, for JSON numbers
  • string, for JSON strings
  • []interface{}, for JSON arrays
  • map[string]interface{}, for JSON objects
  • nil for JSON null

However, in some simple cases the following scheme can work.

type CustomerEntity struct {
    CustomerName string `json:"customer_name"`
    Address      string `json:"customer_address"`
}

type EmployeeEntity struct {
    EmployeeName string `json:"employee_name"`
    ID           int    `json:"employee_id"`
}

If we know that an entity is either an employee or a customer, then we can define an Entity that embeds each:

type Entity struct {
    CustomerEntity
    EmployeeEntity
}

We can give it methods to check whether it's a customer or an employee:

func (s Entity) IsCustomer() bool {
    return s.CustomerEntity != CustomerEntity{}
}
func (s Entity) IsEmployee() bool {
    return s.EmployeeEntity != EmployeeEntity{}
}

Really, these are just checking that at least one field is set.

Then we unmarshal the following JSON:

{
    "entity": {
        "employee_name": "Bob",
        "employee_id": 77
    }
}

Here's a complete example:

import (
    "encoding/json"
    "fmt"
)

type Example struct {
    Entity Entity `json:"entity"`
}

type Entity struct {
    CustomerEntity
    EmployeeEntity
}

func (s Entity) IsCustomer() bool {
    return s.CustomerEntity != CustomerEntity{}
}
func (s Entity) IsEmployee() bool {
    return s.EmployeeEntity != EmployeeEntity{}
}

type CustomerEntity struct {
    CustomerName    string `json:"customer_name"`
    CustomerAddress string `json:"customer_address"`
}

type EmployeeEntity struct {
    EmployeeName string `json:"employee_name"`
    EmployeeID   int    `json:"employee_id"`
}

func main() {
    var example Example
    if err := json.Unmarshal([]byte(`{"entity":{"employee_name":"Bob", "employee_id":77}}`), &example); err != nil {
        panic("won't fail")
    }
    fmt.Printf("%#v\n", example)
    if example.Entity.IsCustomer() {
        fmt.Printf("customer %s lives at %d\n", example.Entity.CustomerName, example.Entity.CustomerAddress)
    }
    if example.Entity.IsEmployee() {
        fmt.Printf("employee %s has id %d\n", example.Entity.EmployeeName, example.Entity.EmployeeID)
    }
}

which outputs

main.Example{Entity:main.Entity{CustomerEntity:main.CustomerEntity{CustomerName:"", CustomerAddress:""}, EmployeeEntity:main.EmployeeEntity{EmployeeName:"Bob", EmployeeID:77}}}
employee Bob has id 77

as we might expect.

There are a few caveats. First, this won't work if there's overlap in either the JSON or the Go field names for the entity types. Second, nothing stops you from (accidentally) initializing some fields in both the customer and employee types and causing it to return true for both IsCustomer and IsEmployee.

If your JSON data has a "type" field, then you could use it to decide what is held instead:

type Entity struct {
    Type string `json:"type"`
    CustomerEntity
    EmployeeEntity
}

although this has the same drawbacks as the other solution mentioned above.

Upvotes: 1

Related Questions