« Back to Index

Go: Complex mapstructure example

View original Gist on GitHub

Tags: #go #serialization

Complex golang mapstructure example.go

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"reflect"
	"strings"
	"time"

	"github.com/mitchellh/mapstructure"
)

func main() {
	var rd *ResponseData

	// The 1 should be converted to `bool` type with value of `true`.
	// The null should become `nil` due to pointer type in ResponseData field.
	r := strings.NewReader(`{"bool": 1, "possibly_null": null}`)
	rc := io.NopCloser(r)

	if err := decodeBodyMap(rc, &rd); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", rd) // &{Bool:true PossiblyNull:<nil>}
}

type ResponseData struct {
	Bool         bool `mapstructure:"bool"`
	PossiblyNull *int `mapstructure:"possibly_null"`
}

// decodeBodyMap is used to decode an HTTP response body into a mapstructure struct.
// It closes `body`.
func decodeBodyMap(body io.ReadCloser, out any) error {
	defer body.Close()

	var parsed any
	dec := json.NewDecoder(body)
	if err := dec.Decode(&parsed); err != nil {
		return err
	}

	return decodeMap(parsed, out)
}

// decodeMap decodes an `in` struct or map to a mapstructure tagged `out`.
// It applies the decoder defaults used throughout go-fastly.
// Note that this uses opposite argument order from Go's copy().
func decodeMap(in, out any) error {
	decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
		DecodeHook: mapstructure.ComposeDecodeHookFunc(
			mapToHTTPHeaderHookFunc(),
			stringToTimeHookFunc(),
		),
		WeaklyTypedInput: true,
		Result:           out,
	})
	if err != nil {
		return err
	}
	return decoder.Decode(in)
}

// mapToHTTPHeaderHookFunc returns a function that converts maps into an
// http.Header value.
func mapToHTTPHeaderHookFunc() mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data any,
	) (any, error) {
		if f.Kind() != reflect.Map {
			return data, nil
		}
		if t != reflect.TypeOf(new(http.Header)) {
			return data, nil
		}

		typed, ok := data.(map[string]any)
		if !ok {
			return nil, fmt.Errorf("cannot convert %T to http.Header", data)
		}

		n := map[string][]string{}
		for k, v := range typed {
			switch tv := v.(type) {
			case string:
				n[k] = []string{tv}
			case []string:
				n[k] = tv
			case int, int8, int16, int32, int64:
				n[k] = []string{fmt.Sprintf("%d", tv)}
			case float32, float64:
				n[k] = []string{fmt.Sprintf("%f", tv)}
			default:
				return nil, fmt.Errorf("cannot convert %T to http.Header", v)
			}
		}

		return n, nil
	}
}

// stringToTimeHookFunc returns a function that converts strings to a time.Time
// value.
func stringToTimeHookFunc() mapstructure.DecodeHookFunc {
	return func(
		f reflect.Type,
		t reflect.Type,
		data any,
	) (any, error) {
		if f.Kind() != reflect.String {
			return data, nil
		}
		if t != reflect.TypeOf(time.Now()) {
			return data, nil
		}

		// Convert it by parsing
		d, ok := data.(string)
		if !ok {
			return nil, errors.New("failed to type assert `data` to a string")
		}
		v, err := time.Parse(time.RFC3339, d)
		if err != nil {
			// DictionaryInfo#get uses it's own special time format for now.
			return time.Parse("2006-01-02 15:04:05", d)
		}
		return v, err
	}
}