POSTS
JSON to XML with Go
One reason Go is interesting to me is for it’s portability. I’m often running programs on Windows machines which get wiped pretty often, so it saves me time when I don’t have to install dependencies like Python or a C++ runtime. With Go, everything is statically compiled into a single binary which runs out of the box. To give it a try, I decided to take a simple task I would normally perform in Python, and do it in Go instead.
What I wanted to do was to convert a nested JSON document into an XML document with a different format. The JSON format looks something like this:
[{
"added": [],
"same": [{
"id": "dec43ae3",
"changeset": "5ad3cec30de2",
"path": "root/project1",
"name": "project1"}],
"timestamp": "Fri Dec 19 03:14:09 2014",
"removed": [],
"diffs": [{
"id": "a2958d4",
"changeset": "afa416e99817",
"path": "root/project2",
"previousChangeset": "931722c97544",
"name": "project2"}]
}]
while the desired XML format looks like this:
<changeSetIds>
<changeSet changeSetId="5ad3cec30de2" path="root/project1" />
<changeSet changeSetId="afa416e99817" path="root/project2" />
</changeSetIds>
In order to parse a JSON object in Go, we have to use the encoding/json package. The Decode function takes in a struct and populates it from the raw JSON data based on the fields of the struct. In the struct definition, we can add annotations to control the decoder behaviour, which is especially useful for specifying the exact JSON field that a struct field maps to. The following program opens a json file and parses it into an array of structs:
package main
import (
"encoding/json"
"os"
"fmt"
)
type ChangeLog struct {
Added []Diff `json:"added"`
Same []Diff `json:"same"`
Diffs []Diff `json:"diffs"`
Removed []Diff `json:"removed"`
Timestamp string `json:"timestamp"`
}
type Diff struct {
Signature string `json:"signature"`
Changeset string `json:"changeset"`
Path string `json:"path"`
PreviousChangeset string `json:"previousChangeset"`
Name string `json:"name"`
}
func parse(filename string) ([]ChangeLog, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
reader := json.NewDecoder(file)
var changes []ChangeLog
err = reader.Decode(&changes)
if err != nil {
return nil, err
}
return changes, nil
}
func main() {
changes, _ := parse("changes.json")
fmt.Printf("(%+v)\n", changes[0])
}
The program gives the following output when run:
({Added:[] Same:[{Signature: Changeset:5ad3cec30de2 Path:root/project1 PreviousChangeset: Name:project1}] Diffs:[{Signature: Changeset:afa416e99817 Path:root/project2 PreviousChangeset:931722c97544 Name:project2}] Removed:[] Timestamp:Fri Dec 19 03:14:09 2014})
Next, we would like to marshal the ChangeLog
struct into our desired XML format. This involves aggregating all the changesets from the “Added”, “Diffs”, and “Same” groups, leaving out the “Removed” group. This part was surprisingly tricky, although I couldn’t find a better way to do it. First, we create a slice which will contain all the changesets, which needs to contain the total length that we expect. That’s because the copy function will only copy up to the current size of the slice. Next, we copy each array into the slice, making sure the offset is incremented by the size of the previously copied array.
total := len(changes[0].Added) + len(changes[0].Same) + len(changes[0].Diffs)
changesets := make([]Diff, total)
i := copy(changesets, changes[0].Added)
i = copy(changesets[i:], changes[0].Diffs) + i
_ = copy(changesets[i:], changes[0].Same)
Finally, we’re ready to marshal our Diff structs into XML. The encoding/xml package gives us XML marshalling/unmarshalling functionality that works similarly to the json package. Since all XML elements must be named, we can enhance our existing Diff struct with an additional field of type xml.Name
to indicate the element name, as well as other annotations to control which fields to ignore, and which fields to set as attributes of the parent element. Note that the extra field does not affect the original JSON parsing, as any unknown fields will just remain as nil.
type Diff struct {
XMLName xml.Name `xml:"changeSet"`
Signature string `json:"signature" xml:"-"`
Changeset string `json:"changeset" xml:"changeSetId,attr"`
Path string `json:"path" xml:"path,attr"`
PreviousChangeset string `json:"previousChangeset" xml:"-"`
Name string `json:"name" xml:"-"`
}
type ChangeSetIds struct {
XMLName xml.Name `xml:"changeSetIds"`
Changesets []Diff
}
The actual code to marshal the XML then becomes real easy:
export := ChangeSetIds{
Changesets: changesets,
}
output, _ := os.Create("changeSet.xml")
defer output.Close()
enc := xml.NewEncoder(output)
enc.Indent("", " ")
_ = enc.Encode(export)
It was pretty fun figuring out how to perform a simple but useful task using Go. Even when taking the learning curve into account, it would have been faster to do it in Python. However, the Go program is easier to maintain because I can exert a lot of fine control on the marshalling behaviour using the struct field annotations.
-
golang