Unmarshal JSON to non-empty interface

When you’re filling a struct by calling :

json.Unmarshal(buff, &yourStruct)

It is not uncommon that your struct contains a non-empty interface.

type IElement interface {
  GetBody() string
}
 
type Message struct {
  Name string
  Provider string
  Elements []IElement
}

And of course, json.Unmarshal can’t automatically find matching structs to fill that []IElement. It’s annoying.

Is it really necessary ?

You could make several messages, each one with a specific struct. Then you could Unmarshal them seperatly, and use them as IElement.

type MessageA struct {
  Name string
  Provider string
  Specific []ElementA
}
 
type MessageB struct {
  Name string
  Provider string
  Specific []ElementB
}
 
// ...

But it’s a pain for API-user, and today, we deal with “developer experience”; we want it as easy as possible.

So, what do we do ?

    1. Get the data.

If you unmarshall a json text to this Message struct, Elements will never be filled.
So, let’s change that.

type Message struct {
  Name string
  Provider string
  Elements []map[string]interface{}
}

Unmarshalling without specific types
interface{} and map[string]interface{} are the 2 types that json unmarshal can always use to fill data.

Hidden for the API-user, we add the Elements as we want them :

type Message struct {
  Name string
  Provider string
  Elements []map[string]interface{}
  ElementsBuilded []IElement `json:"-"`  // <--- New !
}
About `json:”-“`
In every tag, as in `json:”-“`, the dash means “ignore”.
So ElementsBuilded will never be filled by Unmarshall or read by Marshall

And the trick is to fill ElementsBuilded from Elements.

    2. Identify the type

At this step, there is a problem. You don’t know how to recognize the received struct.

type ElementA struct { /* ... */ }
type ElementB struct { /* ... */ }
type ElementC struct { /* ... */ }

So, let’s add a “Type” field (string) to all elements, used to identify the type of element.

// Declaring properly different type 
type ElemKind string
 
const (
  EElemA ElementKind = "a" // I add prefix "E" to explicit that it's an enum kind of const
  EElemB ElementKind = "b"
  EElemC ElementKind = "c"
)
 
// Struct to embed for non-redundant code
type headerElement struct {
   Type string
}
 
// The possibles elements
type ElementA struct {
   headerElement
}
 
type ElementB struct {
   headerElement
}
 
type ElementC struct {
   headerElement
}

At this point, we add a constraint for the API-user : He must add a type “kind” to all struct. But that’s a tiny cost.

    3. From empty interface to non-empty interface

 
budd := "your json input here"
 
var msg Message
err := json.Unmarshal(buff, &msg)
 
if err != nil {
  panic(err)
}
 
for key, elem := range msg.Elements {
 
   // First, we get the field "Type"
   kind, ok := elem["Type"]
   if !ok {
      // Deal with error
      continue
   }
 
   // To Unmarshall to the good struct, we need first
   // to get the original raw text json of the struct
   raw, err := json.Marshal(elem)
   if err != nil {
      // Should not happen
      panic(err)
   }
 
   // Now, for each possible type, we Unmarshall to
   // to the corresponding struct
   switch kind {
     case EElemA :
       var tmp ElementA
       _ = json.Unmarshal(raw, &tmp)
       msg.ElementsBuilded = append(msg.ElementsBuilded, tmp)
 
     case EElemB :
       var tmp ElementB
       _ = json.Unmarshal(raw, &tmp)
       msg.ElementsBuilded = append(msg.ElementsBuilded, tmp)
 
     case EElemC :
       var tmp ElementC
       _ = json.Unmarshal(raw, &tmp)
       msg.ElementsBuilded = append(msg.ElementsBuilded, tmp)
   }
}

Here we are !
Your msg.ElementsBuilded is now ready.

Entrepreneur – Cofounder at Golem.ai (Paris, France)

I enjoy sharing Golang interesting patterns, experiments and tips.

Leave a Reply

Your email address will not be published. Required fields are marked *