Tags: #go #serialization
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
}
}