Generate complex xml with Golang

Oct 20, 2022 by Jeroen Deviaene

Last week I needed a way to generate complex XML export data for a client. This was a project that sounded like a great way for me to dig deeper into Go!
Quickly I discovered how simple it is to generate XML files from structs, however, some parts required deeply nested data and several attributes. Go also supports this, but it took me a while to understand how all tags worked. So I thought I’d give an overview of some of the handy XML tags I used.

The basics

In Go, additional metadata can be added to struct properties in the form of tags. I won’t go over how this works, but know that this metadata is provided between backticks (`) right after the property definition. For generating XML strings, we use the xml key to add information.

Below is a simple example of how we can define the XML tag name for each of the properties in our struct.

type Product struct {
    Id    int     `xml:"product_id"`
    Name  string  `xml:"product_name"`
    Price float32 `xml:"product_price"`
    Brand string  `xml:"product_brand"`
}
<Product>
    <product_id>7</product_id>
    <product_name>Socks</product_name>
    <product_price>7.99</product_price>
    <product_brand>Foo</product_brand>
</Product>

It’s quite simple to define any name for the tags by adding the name after the xml key.

Nested data

Now imagine we need the brand to be contained in a <company> tag. To do this we could make a new struct named Company, move the properties there and reference this struct from our product. However, over time this could create a bunch of structs just to nest single properties.

Go allows defining nested tags in a way like breadcrumbs, you add the different nesting steps in the name field delimited by >. Like so:

type Product struct {
    Id    int     `xml:"product_id"`
    Name  string  `xml:"product_name"`
    Price float32 `xml:"product_price"`
    Brand string  `xml:"product_brand>company"`
}
<Product>
    <product_id>7</product_id>
    <product_name>Socks</product_name>
    <product_price>7.99</product_price>
    <product_brand>
        <company>Foo</company>
    </product_brand>
</Product>

There is no limit on how far you want to nest a single property. But keep in mind if you are nesting too far in, breaking down the struct into smaller structs might yet be a better idea for readability and future maintenance.

Attributes

A new requirement has come in: the price should have a currency attribute.
Adding an attribute is simple, just add ,attr to the end of the XML tag. This will add the value as an attribute with the provided key to the current parent tag. Because of this, we will have to make a separate struct to set the currency attribute on the XML price tag.

Because the price value is now a property as well, it would be exported as a separate XML tag. This is not what we want, it has to be the value of this XML tag. To accomplish this, we can add the ,chardata tag value to the property. This tells Go this property should be set as the value for the parent tag, other child tags will still be exported next to this value.
If the XML tag value contains unknown data, you can also use ,cdata. This does the same as ,chardata, but wraps the data in <![CDATA[ ... ]]>.

type Product struct {
    Id    int    `xml:"product_id"`
    Name  string `xml:"product_name"`
    Price Price  `xml:"product_price"`
    Brand string `xml:"product_brand"`
}

type Price struct {
    Value    float32 `xml:",chardata"`
    Currency string  `xml:"currency,attr"`
}
<Product>
    <product_id>7</product_id>
    <product_name>Socks</product_name>
    <product_price currency="EUR">7.99</product_price>
    <product_brand>Foo</product_brand>
</Product>

Optional attributes

Golang will always export attributes when defined as above. However, there might be instances where you would want the attribute to be omitted when it has no value. For this, you can add ,omitempty to any attribute field. This will remove the attribute entirely if the field value has its type’s default value.
For example, adding this to the Price currency like so:

type Price struct {
    Value    float32 `xml:",chardata"`
    Currency string  `xml:"currency,attr,omitempty"`
}

Footnote

As you can see, Golang has many tools built-in for generating XML files without making your structs overly complex. There are a few more tag modifiers that I did not use in my examples, but all available modifiers can be found in the encode/xml documentation.