pdvrieze / xmlutil

XML Serialization library for Kotlin
https://pdvrieze.github.io/xmlutil/
Apache License 2.0
363 stars 30 forks source link

How to properly serialize optional tag with children #168

Closed super-cooper closed 1 year ago

super-cooper commented 1 year ago

I'm working on an optional tag. De-serialization is working just fine, but I am having issues with serialization. This is my current code:

data class Channel(
    @XmlSerialName(
        "skipHours", "", ""
    ) @XmlChildrenName("hour", "", "") val skipHours: List<Hour>? = null
)
@Serializable
@XmlSerialName("hour", "", "")
data class Hour(@XmlValue(true) val hour: UInt)

As an example, this XML code

<channel>
  <skipHours>
    <hour>5</hour>
    <hour>6</hour>
  </skipHours>
</channel>

correctly de-serializes to a channel with a skipHours list, with two Hours with the correct values. It also works in reverse. If the channel does not contain the skipHours tag, then the skipHours class variable is correctly null.

However, if I have skipHours set to null and attempt to serialize it, it always shows me this:

<channel>
  <skipHours>
  </skipHours>
</channel>

Is there an annotation I can use which will cause a null skipHours to be serialized to nothing, instead of an empty tag?

pdvrieze commented 1 year ago

Hmm. An empty list should indeed be serialized as empty indeed. I'll see what I can do to fix it.

super-cooper commented 1 year ago

Hey, sorry, I was mistaken in opening this issue. I just double checked, and it looks like the code I posted is working. My mistake!

super-cooper commented 1 year ago

It actually looks like I had confused this issue (which I fixed) to another issue which was confusing me. I can open a new issue if you'd like.

Essentially, this class always has its height and width serialized, even if they're the default value:

@XmlSerialName("image", "", "")
data class Image(
    @XmlSerialName("width", "", "") @XmlDefault("$DEFAULT_WIDTH") @XmlElement(true) val width: UInt = DEFAULT_WIDTH,
    @XmlSerialName("height", "", "") @XmlDefault("$DEFAULT_HEIGHT") @XmlElement(true) val height: UInt = DEFAULT_HEIGHT,
)

Should I have these values be nullable and default null instead?

pdvrieze commented 1 year ago

It shouldn't be needed to make them nullable (or to use the @XmlDefault annotation - that annotation predates the proper default support in kotlinx.serialization and allows for some use beyond. There is/should be a configuration option for how defaults are dealt with somewhere.

super-cooper commented 1 year ago

Ah, interesting. I didn't know that was built into kotlinx.serialization. I think I have found what you're referring to here:

https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/basic-serialization.md#defaults-are-not-encoded-by-default

However, they note:

Default values are not encoded by default in JSON

Maybe this behavior is not default for all serialization formats? Either way, it seems I can force it for now with EncodeDefault.Mode. I don't see this enum used anywhere in xmlutil's code.

pdvrieze commented 1 year ago

The behaviour is ultimately decided by XmlSerializationPolicy.shouldEncodeElementDefault The default policy uses the encodeDefault property to set this behaviour. The default value for that is ANNOTATED which basically does not encode if the XmlDefault annotation is present, but does otherwise (this is for compatibility reasons).

Your code should work, I suspect there is some strange interaction with value classes here (as UInt is a value class).

super-cooper commented 12 months ago

Thanks for your help! I'm not sure what the cause is (you of course may be right about value classes), but ultimately, setting @EncodeDefault(EncodeDefault.Mode.NEVER) on both properties did fix the issue.

pdvrieze commented 11 months ago

This should work as the @EncodeDefault annotation works on the serializer, not the format (formats have limited ability to deal with defaults as the values/types are going to be unknown).