In my humble opinion, modifying an XML node value with the Go standard xml package without describing all tags in data structures is not very intuitive. Hence this post is born. There are many ways to do it with their pros and cons. I’ll describe 2 ways. The first is simpler, but loses some XML elements like comments and possibly formatting. The other is more involved but preserves everything from the initial document, including comments and formatting.

Below is the XML document we’ll use as an example and the node we’ll modify is app>description>version. We’d like to set the version value from 1.0 to 2.0. Assume the XML file is called application.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<app>
	<!-- application description -->
	<description>
		<name>My App</name>
		<version>1.0</version>
	</description>
	<!-- developer maintaining the application -->
	<developer>
		<name>Developer Name</name>
		<contacts>
			<email>developer@example.com</email>
			<mobile>+359888888888</mobile>
		</contacts>
	</developer>
	<!-- markets where the app is distributed -->
	<market>
		<googleplay>https://googleplay.com/url</googleplay>
		<appstore>https://appstore.com/url</appstore>
	</market>
</app>

1. The simple way

We are describing the tags/structure that we’d like to modify, but we are not completely describing the other tags/structures like developer and market which we don’t intend to modify.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
	"encoding/xml"
	"fmt"
	"io/ioutil"
	"log"
)

type rawXML struct {
	Inner []byte `xml:",innerxml"`
}

type description struct {
	Name    string `xml:"name"`
	Version string `xml:"version"`
}

type app struct {
	Description description `xml:"description"`
	Developer   rawXML      `xml:"developer"`
	Market      rawXML      `xml:"market"`
}

func main() {
	data, err := ioutil.ReadFile("application.xml")
	if err != nil {
		log.Fatal(err)
	}

	var myapp app
	if err := xml.Unmarshal(data, &myapp); err != nil {
		log.Fatal(err)
	}

	// modify the version value
	myapp.Description.Version = "2.0"

	modified, err := xml.MarshalIndent(&myapp, "", "	")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%s\n", modified)
}

The program will modify the version field, but XML comments from the original document will dissappear. Also formatting may dissappear, though we can specify new formatting by using xml.MarshalIndent(). The pros of this approach are that it’s simple, fairly intuitive (except for the rawXML structure) and very short. But if you want to preserve everything, including the comments, then check out the next approach.

2. The more involved way (streaming parser)

This approach preserves XML comments and is more suitable for huge XML documents as it doesn’t decode the whole document at once in memory, but decodes it element by element, like a stream.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"log"
	"os"
)

type description struct {
	Name    string `xml:"name"`
	Version string `xml:"version"`
}

func main() {
	file, err := os.Open("application.xml")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	var buf bytes.Buffer
	decoder := xml.NewDecoder(file)
	encoder := xml.NewEncoder(&buf)

	for {
		token, err := decoder.Token()
		if err == io.EOF {
			break
		}
		if err != nil {
			log.Printf("error getting token: %v\n", err)
			break
		}

		switch v := token.(type) {
		case xml.StartElement:
			if v.Name.Local == "description" {
				var desc description
				if err = decoder.DecodeElement(&desc, &v); err != nil {
					log.Fatal(err)
				}
				// modify the version value and encode the element back
				desc.Version = "2.0"
				if err = encoder.EncodeElement(desc, v); err != nil {
					log.Fatal(err)
				}
				continue
			}
		}
		
		if err := encoder.EncodeToken(xml.CopyToken(token)); err != nil {
			log.Fatal(err)
		}
	}

	// must call flush, otherwise some elements will be missing
	if err := encoder.Flush(); err != nil {
		log.Fatal(err)
	}

	fmt.Println(string(buf.Bytes()))
}