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: not marshalling nil pointer in struct without ",omitempty" #53408

Closed
martinrode opened this issue Jun 16, 2022 · 13 comments
Closed

Comments

@martinrode
Copy link

The xml marshaler skips nil pointers in struct values even if "omitempty" is not set. This is not what I would expect from encoding. I expect that behaviour if I set ",omitempty" on the struct field, but not without.

The problem is in the stdlib where this code returns nil and is not encoding a value if it is a nil pointer. This is different from how encoding/json behaves where "nil" is encoded into "null" if not ",omitempty" is set.

  marshal.go:

	// Drill into interfaces and pointers.
	// This can turn into an infinite loop given a cyclic chain,
	// but it matches the Go 1 behavior.
	for val.Kind() == reflect.Interface || val.Kind() == reflect.Pointer {
		if val.IsNil() {
// Why are we returning HERE??
   		    return nil
		}
		val = val.Elem()
	}

Documentation https://pkg.go.dev/encoding/xml says this:

- a field with a tag including the "omitempty" option is omitted
  if the field value is empty. The empty values are false, 0, any
  nil pointer or interface value, and any array, slice, map, or
  string of length zero.

From this I understand that "any nil pointer or interface value" WITHOUT the ",omitempty" is NOT omitted during marshalling...

@seankhliao
Copy link
Member

please include example code that demonstrates the issue

@mvdan
Copy link
Member

mvdan commented Jun 16, 2022

Pleaes read the docs - they do state this quite clearly:

Marshal handles an array or slice by marshaling each of the elements. Marshal handles a pointer by marshaling the value it points at or, if the pointer is nil, by writing nothing. Marshal handles an interface value by marshaling the value it contains or, if the interface value is nil, by writing nothing. Marshal handles all other data by writing one or more XML elements containing the data.

@mvdan mvdan closed this as completed Jun 16, 2022
@martinrode
Copy link
Author

Well, to me this is not clear, it's surprising and unnecessary. If you read the docs further, it talks about how it handles structs below the first paragraph which explains the start of the marshalling (imho).

For struct with pointers or interfaces, the ",omitempty" option is useless otherwise. And it is different than MarshalJSON. And there is no way working around this issue. So please re-open the ticket and let's discuss this further.

An example can be found here: https://goplay.space/#1ipwnPTcOwh

Since the current behaviour can be restored by using ",omitempty" on affected struct fields, I think this is a patch to consider.

  • It would bring the behaviour in line with the JSON lib
  • There is no workaround for this problem at the moment unless you implement your own MarshalXML
  • Current users who rely on this behaviour can use ",omitempty" in their structs

And I think it is ok that the documentation says, it ignores NIL interface and NIL pointer when it STARTS MARSHALLING (thus the text at the beginning), but further down when you talk about how structs and struct tags are handled we read this:

The XML element for a struct contains marshaled elements for each of the exported fields of the struct, with these exceptions:

- a field with a tag including the "omitempty" option is omitted
  if the field value is empty. The empty values are false, 0, any
  nil pointer or interface value, and any array, slice, map, or
  string of length zero.

@mvdan
Copy link
Member

mvdan commented Jun 16, 2022

If you think the docs are confusing, there's always the option of sending a patch to make them easier to understand. In my brief skim of the docs, I didn't see a clear way to do that.

As for changing the behavior - unfortunately we cannot change documented behavior due to https://go.dev/doc/go1compat. Supporting the behavior that you want would require either custom code (like you describe) or new API like an option, for which a detailed proposal should be filed instead: https://github.com/golang/proposal

@martinrode
Copy link
Author

To me the docs are pretty clear and I see the current implementation at fault. I am not happy with the way it works at the moment. And it happened in the past that Stdlib feature were refined / matched with the docs. And since the old behaviour can easily be achieved by using ",omitempty", I don't see such a change breaking the promise (which is more about the language itself and not some struct field info interpretation!).

So please re-consider a fix for this in an upcoming version of Go. I am happy to send in a patch which fixes this issue.

As for the doc change, you can simply NOT TALK about "nil pointer" or "interface values" in the ",omitempty" explanation (which is a copy from the MarshalJSON behaviour), as it has no effect whatsoever anyways...

- a field with a tag including the "omitempty" option is omitted
  if the field value is empty. The empty values are false, 0, any array, slice, map, or
  string of length zero. "omitempty" has no effect on nil pointer or interface values as
  those are omitted by default.

@mvdan
Copy link
Member

mvdan commented Jun 16, 2022

I'm going to reopen this for now because you clearly think that there's a bug to be fixed, but I honestly cannot understand how we can change the existing documented behavior without breaking existing programs. Perhaps someone else can understand what I don't seem to be able to.

At the same time, I would ask you to refrain from using bold and all-capital text - it comes off as unnecessarily aggressive :) I understand you may be frustrated, but there's no need to direct that at the maintainers.

@mvdan mvdan reopened this Jun 16, 2022
@seankhliao
Copy link
Member

Next time please actually use the issue template.
What is the expected output for a nil pointer?

@martinrode
Copy link
Author

Thanks for reopening. The bold text was merely for people how skim through long texts, I personally find it much easier to read as it gives the text more structure.

And I can understand that you think the docs are clear, but to me they were not (maybe because I would assume the xml encoding to match the json encoding as closely as possible). The first paragraph talks about the entry point of the marshalling where "nil pointer and interface" are ignored and that is ok. It's the only option we have when we start the encoding.

But once we encode struct values it makes a lot of sense to allow ",omitempty" to have an effect on "nil pointer and interface" value.

As for breaking existing implementations, since the change would only produce more XML, the practical impact will be close to zero. Empty tags are simply ignored and semantically the XML is the same as it expresses the same thing, just with a little more information.

@martinrode
Copy link
Author

Next time please actually use the issue template. What is the expected output for a nil pointer?

Nil pointer would render to 0 bytes.

type S struct {
	A int64
	B *int64
}
s := S{}
expected for the xml:
<S>
  <A>0</A>  // int == 0
  <B></B>    // *int == nil
</S>

I updated my example https://goplay.space/#c8ojsSu5Cp5

@seankhliao
Copy link
Member

That can't be correct, change the field to a string and now you can't differentiate between empty and nil

@martinrode
Copy link
Author

martinrode commented Jun 16, 2022

That can't be correct, change the field to a string and now you can't differentiate between empty and nil

Why does that matter? What matters is that I can steer the encoder to do what is desired using ",omitempty".

If it's nil and "omitempty" is there, we omit the element, if not, we output the element with no content.

Currently we do not know if the struct has B or not at all. And that is the problem for my use case.

@seankhliao
Copy link
Member

That would break existing code, code that currently skips the output for a *string will now output <field></field>, which, depending on the receiver, may be treated as an empty string, a semantically different output vs omitting it.

From https://stackoverflow.com/questions/774192/what-is-the-correct-way-to-represent-null-xml-elements , XML has no standard concept of null that is portable across domains.
The standard library has chosen and documented a particular output, and changing that breaks our compatibility promise.

While unfortunate, at this is infeasible to change, and has well defined escape hatch for user code that wants a different behavior (implement encoding/xml.Marshaler)

@seankhliao seankhliao closed this as not planned Won't fix, can't repro, duplicate, stale Jun 16, 2022
@martinrode
Copy link
Author

I respect your unfortunate decision. Users could have restored the current behaviour using ",omitempty".

Anyways, I do not want to give up easily, so I will suggest a new tag for struct field ",notomitempty" or so, to get the behaviour I need.

Please consider updating the docs at least, see above for a suggestion. "omitempty" talks about "nil pointer and interface" which is clearly not happening and as such a bug in the docs.

@golang golang locked and limited conversation to collaborators Jun 16, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

4 participants