Lee Hambley Lee Hambley - 2 months ago 10
JSON Question

Flattening marshalled JSON structs with anonymous members in Go

Given the following code: (reproduced here at

.)

package main

import (
"encoding/json"
"fmt"
)

type User struct {
Id int `json:"id"`
Name string `json:"name"`
}

type Session struct {
Id int `json:"id"`
UserId int `json:"userId"`
}

type Anything interface{}

type Hateoas struct {
Anything
Links map[string]string `json:"_links"`
}

func MarshalHateoas(subject interface{}) ([]byte, error) {
h := &Hateoas{subject, make(map[string]string)}
switch s := subject.(type) {
case *User:
h.Links["self"] = fmt.Sprintf("http://user/%d", s.Id)
case *Session:
h.Links["self"] = fmt.Sprintf("http://session/%d", s.Id)
}
return json.MarshalIndent(h, "", " ")
}

func main() {
u := &User{123, "James Dean"}
s := &Session{456, 123}
json, err := MarshalHateoas(u)
if err != nil {
panic(err)
} else {
fmt.Println("User JSON:")
fmt.Println(string(json))
}
json, err = MarshalHateoas(s)
if err != nil {
panic(err)
} else {
fmt.Println("Session JSON:")
fmt.Println(string(json))
}
}


I'm attempting to have the rendered JSON look correct in my case that means something like:

User JSON:
{
"id": 123,
"name": "James Dean",
"_links": {
"self": "http://user/123"
}
}
Session JSON:
{
"id": 456,
"userId": 123,
"_links": {
"self": "http://session/456"
}
}


Unfortunately Go is treating the anonymous member as a real named thing, so it's taking the defined type (
Anything
) and naming the JSON thusly:

User JSON:
{
"Anything": {
"id": 123,
"name": "James Dean"
},
"_links": {
"self": "http://user/123"
}
}
Session JSON:
{
"Anything": {
"id": 456,
"userId": 123
},
"_links": {
"self": "http://session/456"
}
}


There's no clear docs on the handling of anonymous members in JSON, from the docs:


Anonymous struct fields are usually marshaled as if their inner exported fields were fields in the outer struct, subject to the usual Go visibility rules amended as described in the next paragraph. An anonymous struct field with a name given in its JSON tag is treated as having that name, rather than being anonymous.

Handling of anonymous struct fields is new in Go 1.1. Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of an anonymous struct field in both current and earlier versions, give the field a JSON tag of "-".


This doesn't make clear if there's a way to flatten out, or hint to the Marshaller what I am trying to do.

I'm certain that there might be, as there is a special case, magic name that has a special meaning to rename the root element of an XML document in the XML marshaller.

In this case, I'm also not attached to the code in any way, my use-case is to have a function that accepts
interface{}, *http.Request, http.ResponseWriter
and write back HATEOAS documents down the wire, switching on the type passed, to infer which links to write back into the JSON. (thus access to the request, for request host, port, scheme, etc, as well as to the type itself to infer the URL and known fields, etc)

Answer

Working playground link: http://play.golang.org/p/_r-bQIw347

The gist of it is this; by using the reflect package we loop over the fields of the struct we wish to serialize and map them to a map[string]interface{} we can now retain the flat structure of the original struct without introducing new fields.

Caveat emptor, there should probably be several checks against some of the assumptions made in this code. For instance it assumes that MarshalHateoas always receives pointers to values.

package main

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

type User struct {
    Id   int    `json:"id"`
    Name string `json:"name"`
}

type Session struct {
    Id     int `json:"id"`
    UserId int `json:"userId"`
}

func MarshalHateoas(subject interface{}) ([]byte, error) {
    links := make(map[string]string)
    out := make(map[string]interface{})
    subjectValue := reflect.Indirect(reflect.ValueOf(subject))
    subjectType := subjectValue.Type()
    for i := 0; i < subjectType.NumField(); i++ {
        field := subjectType.Field(i)
        name := subjectType.Field(i).Name
        out[field.Tag.Get("json")] = subjectValue.FieldByName(name).Interface()
    }
    switch s := subject.(type) {
    case *User:
        links["self"] = fmt.Sprintf("http://user/%d", s.Id)
    case *Session:
        links["self"] = fmt.Sprintf("http://session/%d", s.Id)
    }
    out["_links"] = links
    return json.MarshalIndent(out, "", "    ")
}
func main() {
    u := &User{123, "James Dean"}
    s := &Session{456, 123}
    json, err := MarshalHateoas(u)
    if err != nil {
        panic(err)
    } else {
        fmt.Println("User JSON:")
        fmt.Println(string(json))
    }
    json, err = MarshalHateoas(s)
    if err != nil {
        panic(err)
    } else {
        fmt.Println("Session JSON:")
        fmt.Println(string(json))
    }
}