Ross McFarlane
Ross McFarlane

Reputation: 4254

Go, encoding/xml: How can I marshal self-closing elements?

I'm writing XML from the following struct:

type OrderLine struct {
    LineNumber     string `xml:"LineNumber"`
    Product        string `xml:"Product"`
    Ref            string `xml:"Ref"`
    Quantity       string `xml:"Quantity"`
    Price          string `xml:"Price"`
    LineTotalGross string `xml:"LineTotalGross"`
}

If the Ref field is empty, I'd like the element to display, but be self-closing, i.e.

<Ref />

and not:

<Ref></Ref>

AFAIK, these two are semantically equivalent, but I would prefer a self-closing tag, as it matches the output from other systems. Is this possible?

Upvotes: 20

Views: 5949

Answers (3)

user21618328
user21618328

Reputation: 1

Until go v1.20.3 it's official package "encoding/xml" still not support shortform, so I modified it to support auto close tag. You can find it here. https://github.com/ECUST-XX/xml

Upvotes: 0

user4466350
user4466350

Reputation:

This post provides two solutions that do not rely on regexp and explains the differences between the two.

The first version is memory friendly, but cpu adverse. It implements a writer that replaces occurrences of search by replace within buffered bytes. It tries to write data as soon as possible, preventing large allocation in memory. It is not the best usage of the cpu because if will scan same data multiple times.

package main

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

// Person represents a <person> node in the XML
type Person struct {
    XMLName   xml.Name   `xml:"Players"`
    DataItems []dataItem `xml:"DataItem"`
}

// Skill represents a <skill> node in the XML
type dataItem struct {
    XMLName        xml.Name `xml:"DataItem"`
    Name           string   `xml:"skillName,attr"`
    YearsPracticed int64    `xml:"practice,attr"`
    Level          string   `xml:"level,attr"`
}

func main() {
    players := Person{
        DataItems: []dataItem{
            {Name: "Soccer", YearsPracticed: 3, Level: "Newbie"},
            {Name: "Basketball", YearsPracticed: 4, Level: "State"},
            {Name: "Baseball", YearsPracticed: 10, Level: "National"},
        },
    }
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)

    dst := &Replace{
        Writer:  os.Stdout,
        Search:  []byte("></DataItem>"),
        Replace: []byte("/>"),
    }
    defer dst.Flush()

    enc := xml.NewEncoder(dst)
    enc.Indent("", "  ")
    if err := enc.Encode(players); err != nil {
        fmt.Printf("error: %v\n", err)
    }
}

type Replace struct {
    io.Writer
    Search  []byte
    Replace []byte
    buf     []byte
}

func (s *Replace) Write(b []byte) (n int, err error) {
    s.buf = append(s.buf, b...)
    s.buf = bytes.ReplaceAll(s.buf, s.Search, s.Replace)
    if len(s.buf) > len(s.Search) {
        w := s.buf[:len(s.buf)-len(s.Search)]
        n, err = s.Writer.Write(w)
        s.buf = s.buf[n:]
    }
    return len(b), err
}

func (s *Replace) Flush() (err error) {
    var n int
    n, err = s.Writer.Write(s.buf)
    s.buf = s.buf[n:]
    return
}

The second version is cpu friendly but memory adverse, as it loads the entire data to modify in memory.

package main

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

// Person represents a <person> node in the XML
type Person struct {
    XMLName   xml.Name   `xml:"Players"`
    DataItems []dataItem `xml:"DataItem"`
}

// Skill represents a <skill> node in the XML
type dataItem struct {
    XMLName        xml.Name `xml:"DataItem"`
    Name           string   `xml:"skillName,attr"`
    YearsPracticed int64    `xml:"practice,attr"`
    Level          string   `xml:"level,attr"`
}

func main() {
    players := Person{
        DataItems: []dataItem{
            {Name: "Soccer", YearsPracticed: 3, Level: "Newbie"},
            {Name: "Basketball", YearsPracticed: 4, Level: "State"},
            {Name: "Baseball", YearsPracticed: 10, Level: "National"},
        },
    }
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)
    players.DataItems = append(players.DataItems, players.DataItems...)

    out := new(bytes.Buffer)

    enc := xml.NewEncoder(out)
    enc.Indent("", "  ")
    if err := enc.Encode(players); err != nil {
        fmt.Printf("error: %v\n", err)
    }

    b := bytes.ReplaceAll(out.Bytes(), []byte("></DataItem>"), []byte("/>"))
    os.Stdout.Write(b)
}

Choose one according to your context of execution.

Upvotes: 2

Pimptech
Pimptech

Reputation: 95

I found a way to do it "hacking" marshal package, but I didn't test it. If you want me to show you the link, let me now, then I post it in comments of this reply.

I did some manually code:

package main

import (
    "encoding/xml"
    "fmt"
    "regexp"
    "strings"
)

type ParseXML struct {
    Person struct {
        Name     string `xml:"Name"`
        LastName string `xml:"LastName"`
        Test     string `xml:"Abc"`
    } `xml:"Person"`
}

func main() {

    var err error
    var newPerson ParseXML

    newPerson.Person.Name = "Boot"
    newPerson.Person.LastName = "Testing"

    var bXml []byte
    var sXml string
    bXml, err = xml.Marshal(newPerson)
    checkErr(err)

    sXml = string(bXml)

    r, err := regexp.Compile(`<([a-zA-Z0-9]*)><(\\|\/)([a-zA-Z0-9]*)>`)
    checkErr(err)
    matches := r.FindAllString(sXml, -1)

    fmt.Println(sXml)

    if len(matches) > 0 {
        r, err = regexp.Compile("<([a-zA-Z0-9]*)>")
        for i := 0; i < len(matches); i++ {

            xmlTag := r.FindString(matches[i])
            xmlTag = strings.Replace(xmlTag, "<", "", -1)
            xmlTag = strings.Replace(xmlTag, ">", "", -1)
            sXml = strings.Replace(sXml, matches[i], "<"+xmlTag+" />", -1)

        }
    }

    fmt.Println("")
    fmt.Println(sXml)

}

func checkErr(chk error) {
    if chk != nil {
        panic(chk)
    }
}

Upvotes: 1

Related Questions