« Back to Index

Go: custom Unmarshal using json.RawMessage

View original Gist on GitHub

Tags: #go #json #serialization

API example.go

// https://play.golang.org/p/_lYDvs0Ukux
//
// Demonstrates how to handle a complex data structure that doesn't work well with golang.
// We write a custom unmarshal that puts the data into a more appropriate data structure.

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

func main() {
	var d DomainValidationResult
	err := json.Unmarshal([]byte(data), &d)
	log.Printf("%+v (err %v)", h, err)
}

const data = `
  [
    {
      "comment": "",
      "name": "www.example.com",
      "service_id": "SU1Z0isxPaozGVKXdv0eY",
      "version": 1,
      "created_at": "2020-03-15T20:10:09.000Z",
      "updated_at": "2020-03-15T20:10:09.000Z",
      "deleted_at": null
    },
    "global.prod.fastly.net.",
    true
  ]
`

// DomainValidationResult defines an idiomatic representation of the API
// response.
type DomainValidationResult struct {
	Metadata DomainMetadata
	Name     string
	Valid    bool
}

// UnmarshalJSON works around the badly designed API response by coercing the
// raw data into a more appropriate data structure.
func (d *DomainValidationResult) UnmarshalJSON(data []byte) error {
	var tuple []json.RawMessage
	if err := json.Unmarshal(data, &tuple); err != nil {
		return fmt.Errorf("initial: %w", err)
	}

	if want, have := 3, len(tuple); want != have {
		return fmt.Errorf("unexpected array length: want %d, have %d", want, have)
	}

	if err := json.Unmarshal(tuple[0], &d.Metadata); err != nil {
		return fmt.Errorf("metadata: %w", err)
	}

	if err := json.Unmarshal(tuple[1], &d.Name); err != nil {
		return fmt.Errorf("name: %w", err)
	}

	if err := json.Unmarshal(tuple[2], &d.Valid); err != nil {
		return fmt.Errorf("valid: %w", err)
	}

	return nil
}

// DomainMetadata represents a domain name configured for a Fastly service.
type DomainMetadata struct {
	Comment   string     `json:"comment"`
	Name      string     `json:"name"`
	ServiceID string     `json:"service_id"`
	Version   int        `json:"version"`
	CreatedAt *time.Time `json:"created_at"`
	UpdatedAt *time.Time `json:"updated_at"`
	DeletedAt *time.Time `json:"deleted_at"`
}

Golang custom Unmarshal using json.RawMessage.go

package main

import (
	"encoding/json"
	"fmt"
)

func main() {
	var v Component

	if err := json.Unmarshal(data, &v); err != nil {
		panic(err)
	}

	fmt.Printf("%#v\n", v)
}

var data = []byte(`{"type": "foo", "config": {"name": "test"}}`)
// THE OTHER TYPE FOR COMPARISON >> var data = []byte(`{"type": "bar", "config": {"value": 123}}`)

type Component struct {
	Type   string      `json:"type"`
	Config interface{} `json:"config"`
}

func (c *Component) UnmarshalJSON(data []byte) error {

	var v struct {
		Type string          `json:"type"`
		Data json.RawMessage `json:"config"`
	}

	if err := json.Unmarshal(data, &v); err != nil {
		return err
	}

	c.Type = v.Type

	switch v.Type {
	case "foo":
		var f FooConfig

		if err := json.Unmarshal(v.Data, &f); err != nil {
			return err
		}

		c.Config = f
	case "bar":
		var b BarConfig

		if err := json.Unmarshal(v.Data, &b); err != nil {
			return err
		}

		c.Config = b
	}

	return nil
}

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

type BarConfig struct {
	Value int `json:"value"`
}