Serialisation: JSON

Go

APIs

Go has a standard package encoding/json with two major functions json.Encoder() and json.Decoder() which encode and decode to a stream, and json.Unmarshal() and json.Marshal() which unmarshal and marshal to and from a byte array.

From the Go JSON package specification, marshalling uses the following type-dependent default encodings:

There is a JSON schema to Go package Go Jsonschema . Applying this to the person schema by


      ./gojsonschema-linux-amd64 -p person -o src/person/Person.go person.schema.json
  
This will need to be built by

      go build src/person/Person.go
  
where the current directory is in GOPATH.

The resultant Person.go is Person.go:


// THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT.

package person

import "fmt"
import "encoding/json"

type Person struct {
	// Email corresponds to the JSON schema field "email".
	Email []PersonEmailElem `json:"email"`

	// Name corresponds to the JSON schema field "name".
	Name PersonName `json:"name"`
}

type PersonEmailElem struct {
	// Address corresponds to the JSON schema field "address".
	Address *string `json:"address,omitempty"`

	// Kind corresponds to the JSON schema field "kind".
	Kind *string `json:"kind,omitempty"`
}

type PersonName struct {
	// Family corresponds to the JSON schema field "family".
	Family *string `json:"family,omitempty"`

	// Personal corresponds to the JSON schema field "personal".
	Personal *string `json:"personal,omitempty"`
}

// UnmarshalJSON implements json.Unmarshaler.
func (j *Person) UnmarshalJSON(b []byte) error {
	var raw map[string]interface{}
	if err := json.Unmarshal(b, &raw); err != nil {
		return err
	}
	if v, ok := raw["email"]; !ok || v == nil {
		return fmt.Errorf("field email: required")
	}
	if v, ok := raw["name"]; !ok || v == nil {
		return fmt.Errorf("field name: required")
	}
	type Plain Person
	var plain Plain
	if err := json.Unmarshal(b, &plain); err != nil {
		return err
	}
	*j = Person(plain)
	return nil
}

func (name PersonName) String() string {
	return fmt.Sprintf("Name{Personal: %s, Family: %s}", *name.Personal, *name.Family)
}

func (email PersonEmailElem) String() string {
	return fmt.Sprintf("Email{Address: %s, Kind: %s}", *email.Address, *email.Kind)
}

It defines the types Person, PersonEmailElem and PersonName

The JSON encoder maps Go type names to JSON field names. Sometimes, the best Go name for a type may not match the corresponding JSON name. Go allows for a type annotation to control the mapping by `json:"string"`. For example, a Person struct may be defined as


type Person struct {
	// Email corresponds to the JSON schema field "email".
	Email []PersonEmailElem `json:"email"`

	// Name corresponds to the JSON schema field "name".
	Name PersonName `json:"name"`
}
  

Notice that the string fields are pointers, not the strings themselves. There is a standard way of pretty printing objects using the fmt package:


      fmt.Printf("%v\n", person)
  
Unfortunately, for pointer types, it prints the pointer value, not the value pointed at. As discussed at golang how to print struct value with pointer This can be remedied by implementing the Stringer interface. Basically, it means adding to Person.go the following:

func (name PersonName) String() string {
	return fmt.Sprintf("Name{Personal: %s, Family: %s}", *name.Personal, *name.Family)
}

func (email PersonEmailElem) String() string {
	return fmt.Sprintf("Email{Address: %s, Kind: %s}", *email.Address, *email.Kind)
}
  
But Person.go is automatically generated, so any addition like this will get overwritten on a regeneration. If I get time, I will fork the project to automatically generate this extra code.

JSON client

The client constructs a Person object using the generated API. It then connects as with the TCP example and sends the JSON serialised data to the server.

The client is PersonClient.go:


/* JSON PersonClient
*/

package main

import (
        "person"
        "encoding/json"
        "fmt"
        "net"
        "os"
)

func main() {
	if len(os.Args) != 2 {
		fmt.Fprint(os.Stderr, "Usage: ", os.Args[0], " host:port\n")
		os.Exit(1)
	}
	service := os.Args[1]

	// build a Person
	toaddr := func(s string) *string { return &s}

	name := person.PersonName{Family: toaddr("Newmarch"),
		                  Personal: toaddr("Jan")}
	email1 := person.PersonEmailElem{Kind: toaddr("home"),
		                         Address: toaddr("jan@newmarch.name")}
	email2 := person.PersonEmailElem{Kind: toaddr("work"),
		                         Address: toaddr("j.newmarch@boxhill.edu.au")}
	emails := []person.PersonEmailElem{email1, email2}
	person := person.Person{Name: name,
                                Email: emails}

	// the built object
	fmt.Printf("%v\n", person)

	// get JSON bytes
	bytes, _ := json.Marshal(person)

	// and send to server
	conn, err := net.Dial("tcp", service)
	checkError("Dial", err)
	fmt.Println("Connected")

	conn.Write(bytes)
	conn.Close()

	os.Exit(0)
}

func checkError(errStr string, err error) {
	if err != nil {
		fmt.Fprint(os.Stderr, errStr, err.Error())
		os.Exit(1)
	}
}

The client is run by


      go run PersonClient.go localhost:2002
  

The output on the client side shows the built object:


{[Email{Address: jan@newmarch.name, Kind: home} Email{Address: j.newmarch@boxhill.edu.au, Kind: work}] Name{Personal: Jan, Family: Newmarch}}
Connected
  

JSON server

The server acts like the TCP servers discussed earlier. This one just receives a single packet and then unmarshals it from JSON. Since JSON doesn't include its type information, an 'empty' Person object has to be passed as parameter.

The server is PersonServer.go:


/* JSON PersonServer
 */
package main

import (
	"person"
        "encoding/json"
	"fmt"
	"net"
	"os"
)

func main() {

	service := ":2002"
	tcpAddr, err := net.ResolveTCPAddr("tcp", service)
	checkError(err)

	listener, err := net.ListenTCP("tcp", tcpAddr)
	checkError(err)

	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		fmt.Println("Connected")
		// run as a coroutine
		go handleClient(conn)
	}
}

func handleClient(conn net.Conn) {
	// close connection on exit
        defer conn.Close()

	var data []byte
	data = make([]byte, 2048, 2048)

	n, err := conn.Read(data)
	if err != nil {
		fmt.Println("Disconnecting")
		return
	}
	person := person.Person{}
	err = json.Unmarshal(data[:n], &person)
	if err != nil {
		fmt.Println("Not a person")
		return
	}
	fmt.Printf("%v\n", person)
}

func checkError(err error) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
		os.Exit(1)
	}
}

The server is run by


      go run PersonServer.go
  
The src directory containing /person/Person.go must be eachable from the GOPATH.

The server will print the object it creates as


      {[Email{Address: jan@newmarch.name, Kind: home} Email{Address: j.newmarch@boxhill.edu.au, Kind: work}] Name{Personal: Jan, Family: Newmarch}}
  

Resources


Copyright © Jan Newmarch, jan@newmarch.name
Creative Commons License
" Network Programming using Java, Go, Python, Rust, JavaScript and Julia" by Jan Newmarch is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License .
Based on a work at https://jan.newmarch.name/NetworkProgramming/ .

If you like this book, please contribute using PayPal