Working with JSON in Go

[ go  json  ]
Written on November 26, 2017

Working with JSON in Go is quite nice. I’m using it as my communication protocol for my exsim project.

To simplify things in Go I ended up changing my command structure a bit, to make it more structured, so to speak. Go is a strongly typed language, but with a bit of planning it is easy to have the JSON encoder/decoder map things to a set of structures, even when the commands have varying parameters.

All commands sent from the server to the physics simulation are of the following format:

{ 
  "command": <command name>,
  "params": <command parameters>
}

For example:

{
  "command": "addship",
   "params": {
    "owner": 1, 
    "typeid": 2, 
    "position": {"x": -10.0, "y": 10.0, "z": 0.0}
   }
}

or

{
  "command": "removeship", 
  "params": {"owner": 1}
}

This can be mapped to a Go structure like this:

type Command struct {
	Command string `json:"command"`
	Params interface{} `json:"params"`
}

The parameters for the commands are separate structures:

type ParamsAddShip struct {
	Owner int64 `json:"owner"`
	TypeId int64 `json:"typeid"`
	Position Vector3 `json:"position"`
}

type ParamsRemoveShip struct {
	Owner int64 `json:"owner"`
}

I have some convenience functions for creating these command structures:

func NewCommand(name string, params interface{}) (*Command){
	return &Command{
		Command: name,
		Params: params,
	}
}

func NewAddShipCommand(owner int64, typeid int64, pos Vector3) (*Command){
	params := &ParamsAddShip{
		Owner: owner,
		TypeId: typeid,
		Position: pos,
	}
	return NewCommand("addship", params)
}

func NewRemoveShipCommand(owner int64) (*Command) {
	params := &ParamsRemoveShip{
		Owner: owner,
	}
	return NewCommand("removeship", params)
}

The structure tags are needed to specify the field names in lower case. In Go, field names starting with lower case characters are private - I need them to be public, but I want the field names in the JSON to be in lower case. It’s a bit silly, but at least Go has the notion of structure tags that allow me to address the issue. Of course the field names could be something very different - the JSON encoder/decoder just looks at the tags.

A JSON Encoder is constructed with a writer. A TCP socket connection is a writer, so I can encode commands directly to the socket that connects the solar system on the server with the physics simulation.

Similarly, a JSON decoder is constructed with a reader. The TCP socket connection is not only a writer, it is an io.ReadWriter so I can decode directly from the socket. This means I don’t have to worry about reading in whole commands to send to the JSON decoder, it simply pulls data from the connection as needed. This also means that the JSON decoding will block while waiting for data, but that is fine, I just run that on a Go routine.

The command results are similar structures - I get back something like this:

{
  "result": "ok"
}

or in the case of the getstate command:

{
  "result": "state"
  "state": <description of state>
}

This can be decoded into a structure like this:

type CommandResult struct {
	Result string `json:"result"`
	State  json.RawMessage `json:"state"`
}

The json.RawMessage allows me to decode the CommandResult while postponing the decoding of the State field. Once I’ve determined the value of the Result field, I call json.Unmarshal to decode its contents to the appropriate structure.

type Vector3 struct {
	X float64 `json:"x"`
	Y float64 `json:"y"`
	Z float64 `json:"z"`
}

type ShipData struct {
	Owner    int64 `json:"owner"`
	TypeId     int64 `json:"typeid"`
	Position Vector3 `json:"position"`
	InRange  []int64 `json:"inrange"`
	NewInRange  []int64 `json:"newinrange"`
	GoneFromRange  []int64 `json:"gonefromrange"`
}

type State struct {
	Ships map[string]ShipData `json:"ships"`
}

...
cmd = exsim_commands.NewGetStateCommand()
result, err = ss.sendCommand(cmd)
if err != nil {
    fmt.Printf("Error in getstate: %v\n", err)
    return err
}

var state exsim_commands.State
json.Unmarshal(result.State, &state)
...

The sendCommand function is simply an Encode call followed by a Decode call:

func (ss *Solarsystem) sendCommand(cmd *exsim_commands.Command) (*exsim_commands.CommandResult, error) {
	ss.encoder.Encode(cmd)

	var result exsim_commands.CommandResult
	err := ss.decoder.Decode(&result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

So, I’m pretty happy with working with JSON in Go :)