In parsing XML with Go, how can I read a nested element's attribute directly into my struct?
My XML looks like this below. It is part of the OpenStreetMap format:
<way id="123" >
<nd ref="101"/>
<!-- Lots of nd elements repeated -->
<nd ref="109"/>
</way>
I have
type Way struct {
Nodes []NodeRef `xml:"nd"`
}
with
type NodeRef struct {
Ref int `xml:"ref,attr"`
}
but I would like to be able to do
type Way struct {
Nodes []int `???`
}
directly.
The documentation on Unmarshalling didn't help me. I've tried using xml:"nd>ref,attr"
but that fails with "chain not valid with attr flag".
Please see the below example code. Run the code in Go Playground
package main
import (
"encoding/xml"
"fmt"
"io"
"os"
"strings"
)
func main() {
data := `
<way id="5090250" >
<nd ref="822403"/>
<nd ref="21533912"/>
<nd ref="821601"/>
<nd ref="21533910"/>
<nd ref="135791608"/>
<nd ref="333725784"/>
<nd ref="333725781"/>
<nd ref="333725774"/>
<nd ref="333725776"/>
<nd ref="823771"/>
</way>
`
r := strings.NewReader(data)
way, err := ReadWay(r)
if err != nil {
fmt.Println("Could not read", err)
os.Exit(1)
}
fmt.Println(way)
}
// I'd like to get rid of this nested struct.
type NodeRef struct {
Ref int `xml:"ref,attr"`
}
type Way struct {
ID int `xml:"id,attr"`
// How can I write all <nd ref="123"/> directly into Nodes []int?
Nodes []NodeRef `xml:"nd"`
}
func ReadWay(reader io.Reader) (Way, error) {
var way Way
if err := xml.NewDecoder(reader).Decode(&way); err != nil {
return way, err // Why can't I return nil, err?
}
return way, nil
}
In short, you can't do that directly. The common pattern to bypass the XML structure is to implement the xml.Unmarshaler
interface. Here is an example:
type Way struct {
ID int
Nodes []int
}
func (w *Way) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var payload struct {
ID int `xml:"id,attr"`
Nodes []struct {
Ref int `xml:"ref,attr"`
} `xml:"nd"`
}
err := d.DecodeElement(&payload, &start)
if err != nil {
return err
}
w.ID = payload.ID
w.Nodes = make([]int, 0, len(payload.Nodes))
for _, node := range payload.Nodes {
w.Nodes = append(w.Nodes, node.Ref)
}
return nil
}
Here, the payload
variable is modeled after your XML data, while the Way
struct is modeled as you want it to be. Once the payload is decoded, you can use it to initialize the Way
variable as you want…
Side note: // Why can't I return nil, err?
You can't return nil
because Way
isn't either a pointer or an interface, thus nil
isn't a valid value for it.