Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Flattening marshalled JSON structs with anonymous members in Go

Given the following code: (reproduced here at play.golang.org.)

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)

like image 649
Lee Hambley Avatar asked Dec 03 '13 21:12

Lee Hambley


2 Answers

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))
    }
}
like image 85
Kristoffer Sall-Storgaard Avatar answered Nov 19 '22 05:11

Kristoffer Sall-Storgaard


sorry, but I think the JSON you're trying to generate is not a valid JSON object and thus it may be the reason the JsonMarshal is not playing game with you.

The object may not consumable via JavaScript as it contains two objects, unless you wrap the objects in an array.

[
    {
        "id": 123,
        "name": "James Dean",
        "_links": {
            "self": "http://user/123"
        }
    },
    {
        "id": 456,
        "userId": 123,
        "_links": {
            "self": "http://session/456"
        }
    }
]

Then you would be able to consume this JSON, example:

var user, session;
user = jsonString[0]; 
session = jsonString[1];

Consider giving your objects root names might be a better consideration, example:

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

and consumed as, example:

var user, session;
user = jsonString.user;
session = jsonString.session;

I hope this helps you

like image 22
kylewelsby Avatar answered Nov 19 '22 05:11

kylewelsby