Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

encoding/xml: cannot unmarshal into without struct tags #65099

Open
daenney opened this issue Jan 14, 2024 · 1 comment
Open

encoding/xml: cannot unmarshal into without struct tags #65099

daenney opened this issue Jan 14, 2024 · 1 comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@daenney
Copy link

daenney commented Jan 14, 2024

Go version

go version go1.21.6 linux/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN='/home/daenney/.local/bin'
GOCACHE='/home/daenney/.cache/go-build'
GOENV='/home/daenney/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/daenney/.cache/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/daenney/.cache/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/go'
GOSUMDB='sum.golang.org'
GOTMPDIR='/run/user/1000'
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/lib/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.21.6'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/daenney/Development/github.com/daenney/xmldecode/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/run/user/1000/go-build4263511798=/tmp/go-build -gno-record-gcc-switches'

What did you do?

I'm filing this one as a bug since the behaviour took me by surprise and wasn't obvious to me from the documentation on encoding/xml.Unmashal.

I tried to decode a response into a struct where:

  • The member I'm decoding into is a slice of structs
  • The member doesn't have an xml tag set
  • The slice members are structs, which has an XMLName set
<response>
  <info>
    <lastmodified>2024-01-14T10:53:21Z</lastmodified>
  </info>
  <a><id>1</id></a>
  <a><id>2</id></a>
</response>
type A3 struct {
	XMLName xml.Name `xml:"a"`
	ID      string   `xml:"id"`
}

type Response3 struct {
	XMLName      xml.Name `xml:"response"`
	Data         []A3
	LastModified *time.Time `xml:"info>lastmodified"`
}

I've included the full code in a Gist, because unfortunately go.dev/play continuously returns server errors when using the "Share" button: https://gist.github.com/daenney/07e546e0c9d26e5ba183feadd3ba00c4

What did you see happen?

Whether a single, or multiple <a> elements are present, .Data remains empty, whereas I expected it to accumulate into .Data given the XMLName is present on the slice member struct.

&{XMLName:{Space: Local:response} Data:[] LastModified:2024-01-14 10:53:21 +0000 UTC}
&{XMLName:{Space: Local:response} Data:[] LastModified:2024-01-14 10:53:21 +0000 UTC}

What did you expect to see?

It feels like the following two things should be equivalent and behave the same:

type A2 struct {
	ID string `xml:"id"`
}

type Response2 struct {
	XMLName      xml.Name   `xml:"response"`
	Data         []A2       `xml:"a"`
	LastModified *time.Time `xml:"info>lastmodified"`
}
type A3 struct {
	XMLName xml.Name `xml:"a"`
	ID      string   `xml:"id"`
}

type Response3 struct {
	XMLName      xml.Name `xml:"response"`
	Data         []A3
	LastModified *time.Time `xml:"info>lastmodified"`
}

Clearly they're not since []A3 doesn't directly have a tag and it seems the implementation doesn't take the XMLName of the slice members into account. It's not obvious from the documentation that in the case of a slice either the member name or the struct tag needs to match the XML element name for the decoding to occur. Even if I create a named type like type Datas []T, I can't attach the right xml tag to it nor give it an XMLName member since it's not a struct.

This isn't typically a problem as you can always write the xml:"a" tag, until we throw generics in the mix. Many APIs have a envelope, like seen here, with stuff like an info or error member in their response and the data elements, where the data elements can be a different type based on the query/endpoint. It would be nice to be able to define something like:

type Response[T Responses] struct {
	XMLName      xml.Name `xml:"response"`
	Data         []T
	LastModified *time.Time `xml:"info>lastmodified"`
}

type Responses interface {
	RespA | RespB
}

type RespA struct{}
type RespB struct{}

There is one, hacky, way to make this work:

type Response4[T Responses4] struct {
	XMLName      xml.Name   `xml:"response"`
	Data         []T        `xml:",any"`
	LastModified *time.Time `xml:"info>lastmodified"`
}

Due to the xml:",any" we now get the intended behaviour. But this is only safe if the wrapping Response type defines all other fields upfront, otherwise different elements could try to accumulate into Response.Data and Unmashal would fail.

@dmitshur dmitshur added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Jan 15, 2024
@dmitshur dmitshur added this to the Backlog milestone Jan 15, 2024
@dmitshur
Copy link
Contributor

CC @rsc, @ianlancetaylor.

@seankhliao seankhliao changed the title encoding/xml: Cannot decode into slice member if member name doesn't match XML element name and is missing xml tag despite XMLName being present on slice type encoding/xml: cannot unmarshal into without struct tags Jan 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

2 participants