This example is based upon bug #41. It shows how to use a custom serializer with filtering to handle an "almostXml" format.
<Container>
<Test_123 attr="42">
<data>someData</data>
</Test_123>
<Test_456 attr="71">
<data>moreData</data>
</Test_456>
</Container>Container & TestElement - data.kt
/** Version that works with the released version 0.80.0 and 0.80.1 */
@Serializable(with = ContainerSerializer::class)
data class Container(val data: List<TestElement>)
@Serializable
data class TestElement(val id: Int, val attr: Int, @XmlElement(true) val data: String)There are 2 example serializers. One works in 0.80.0 and 0.80.1 and the other one will work on the master/dev branches only. Almost all code is shared though.
/**
* The compatible serializer doesn't have access to state to determine a proper delegate format. This
* implementation uses the default format instance. It is perfectly valid however to create a custom
* instance (for example providing a SerialModule) here and return that from both delegate methods
*/
object CompatContainerSerializer: CommonContainerSerializer() {
override fun delegateFormat(decoder: Decoder) = XML.defaultInstance
override fun delegateFormat(encoder: Encoder) = XML.defaultInstance
}/**
* This version of the serializer uses the new delegateFormat method on [XML.XmlInput] and [XML.XmlOutput]
* to inherit configuration and serializerModules.
*/
object ContainerSerializer: CommonContainerSerializer() {
override fun delegateFormat(decoder: Decoder) = (decoder as XML.XmlInput).delegateFormat()
override fun delegateFormat(encoder: Encoder) = (encoder as XML.XmlOutput).delegateFormat()
}/**
* A common base class that contains the actual code needed to serialize/deserialize the container.
*/
abstract class CommonContainerSerializer: KSerializer<Container> {
/** We need to have the serializer for the elements */
private val elementSerializer = serializer<TestElement>()
/** Autogenerated descriptors don't work correctly here. */
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Container") {
element("data", ListSerializer(elementSerializer).descriptor)
}
override fun deserialize(decoder: Decoder): Container {
// XmlInput is designed as an interface to test for to allow custom serializers
if (decoder is XML.XmlInput) { // We treat XML different, using a separate method for clarity
return deserializeDynamic(decoder, decoder.input)
} else { // Simple default decoder implementation that delegates parsing the data to the ListSerializer
val data = decoder.decodeStructure(descriptor) {
decodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer))
}
return Container(data)
}
}
/**
* This function is the meat to deserializing the container with dynamic tag names. Note that
* because we use xml there is no point in going through the (anonymous) list dance. Doing that
* would be an additional complication.
*/
fun deserializeDynamic(decoder: Decoder, reader: XmlReader): Container {
val xml = delegateFormat(decoder) // get the format for deserializing
// We need the descriptor for the element. xmlDescriptor returns a rootDescriptor, so the actual descriptor is
// its (only) child.
val elementXmlDescriptor = xml.xmlDescriptor(elementSerializer).getElementDescriptor(0)
// A list to collect the data
val dataList = mutableListOf<TestElement>()
decoder.decodeStructure(descriptor) {
// Finding the children is actually not left to the serialization framework, but
// done by "hand"
while (reader.next() != EventType.END_ELEMENT) {
when (reader.eventType) {
EventType.COMMENT,
EventType.IGNORABLE_WHITESPACE -> {
// Comments and whitespace are just ignored
}
EventType.ENTITY_REF,
EventType.TEXT -> if (reader.text.isNotBlank()) {
// Some parsers can return whitespace as text instead of ignorable whitespace
// Use the handler from the configuration to throw the exception.
xml.config.unknownChildHandler(reader, InputKind.Text, null, emptyList())
}
// It's best to still check the name before parsing
EventType.START_ELEMENT -> if(reader.namespaceURI.isEmpty() && reader.localName.startsWith("Test_")) {
// When reading the child tag we use the DynamicTagReader to present normalized XML to the
// deserializer for elements
val filter = DynamicTagReader(reader, elementXmlDescriptor)
// The test element can now be decoded as normal (with the filter applied)
val testElement = xml.decodeFromReader(elementSerializer, filter)
dataList.add(testElement)
} else { // handling unexpected tags
xml.config.unknownChildHandler(reader, InputKind.Element, reader.name, listOf("Test_??"))
}
else -> { // other content that shouldn't happen
throw XmlException("Unexpected tag content")
}
}
}
}
return Container(dataList)
}
override fun serialize(encoder: Encoder, value: Container) {
if (encoder is XML.XmlOutput) { // When we are using the xml format use the serializeDynamic method
return serializeDynamic(encoder, encoder.target, value.data)
} else { // Otherwise just manually do the encoding that would have been generated
encoder.encodeStructure(descriptor) {
encodeSerializableElement(descriptor, 0, ListSerializer(elementSerializer), value.data)
}
}
}
/**
* This function provides the actual dynamic serialization
*/
fun serializeDynamic(encoder: Encoder, target: XmlWriter, data: List<TestElement>) {
val xml = delegateFormat(encoder) // get the format for deserializing
// We need the descriptor for the element. xmlDescriptor returns a rootDescriptor, so the actual descriptor is
// its (only) child.
val elementXmlDescriptor = xml.xmlDescriptor(elementSerializer).getElementDescriptor(0)
encoder.encodeStructure(descriptor) { // create the structure (will write the tags of Container)
for (element in data) { // write each element
// We need a writer that does the renaming from the normal format to the dynamic format
// It is passed the string of the id to add.
val writer = DynamicTagWriter(target, elementXmlDescriptor, element.id.toString())
// Normal delegate writing of the element
xml.encodeToWriter(writer, elementSerializer, element)
}
}
}
// These functions abstract away getting the delegate format in improved or compat way.
abstract fun delegateFormat(decoder: Decoder): XML
abstract fun delegateFormat(encoder: Encoder): XML
}This is supported by two filters (one for reading and writing).
/**
* A filter that reads xml with dynamic tags and represents it as a structured xml with id attribute
*/
internal class DynamicTagReader(reader: XmlReader, descriptor: XmlDescriptor) : XmlDelegatingReader(reader) {
private var initDepth = reader.depth
private val filterDepth: Int
/**
* We want to be safe so only handle the content at relative depth 0. The way that depth is determined
* means that the depth is the depth after the tag (and end tags are thus one level lower than the tag (and its
* content). We correct for that here.
*/
get() = when (eventType) {
EventType.END_ELEMENT -> delegate.depth - initDepth + 1
else -> delegate.depth - initDepth
}
/**
* Store the tag name that we need to use instead of the dynamic tag
*/
private val elementName = descriptor.tagName
/**
* Store the name of the id attribute that is synthetically generated. This property is initialised
* this way to allow for name remapping in the format policy.
*/
private val idAttrName = (0 until descriptor.elementsCount)
.first { descriptor.serialDescriptor.getElementName(it) == "id" }
.let { descriptor.getElementDescriptor(it) }
.tagName
/**
* This filter is created when we are at the local tag. So we can already determine the value of the
* synthetic id property. In this case just by removing the prefix.
*/
val idValue = delegate.localName.removePrefix("Test_")
/**
* When we are at relative depth 0 we add an attribute at position 0 (easier than at the end). This allows
* for other attributes (actually written) on the tag.
*/
override val attributeCount: Int
get() = when (filterDepth) {
0 -> super.attributeCount + 1
else -> super.attributeCount
}
/**
* At relative depth 0, attribute 0 we inject the namespace for the id attribute. The other attribute indices are just
* adjusted.
*/
override fun getAttributeNamespace(index: Int): String = when (filterDepth) {
0 -> when (index) {
0 -> idAttrName.namespaceURI
else -> super.getAttributeNamespace(index - 1)
}
else -> super.getAttributeNamespace(index)
}
/**
* At relative depth 0, attribute 0 we inject the prefix for the id attribute. The other attribute indices are just
* adjusted.
*/
override fun getAttributePrefix(index: Int): String = when (filterDepth) {
0 -> when (index) {
0 -> idAttrName.prefix
else -> super.getAttributePrefix(index - 1)
}
else -> super.getAttributePrefix(index)
}
/**
* At relative depth 0, attribute 0 we inject the local name for the id attribute. The other attribute indices are just
* adjusted.
*/
override fun getAttributeLocalName(index: Int): String = when (filterDepth) {
0 -> when (index) {
0 -> idAttrName.localPart
else -> super.getAttributeLocalName(index - 1)
}
else -> super.getAttributeLocalName(index)
}
/**
* At relative depth 0, attribute 0 we inject the value for the id attribute. The other attribute indices are just
* adjusted.
*/
override fun getAttributeValue(index: Int): String = when (filterDepth) {
0 -> when (index) {
0 -> idValue
else -> super.getAttributeValue(index - 1)
}
else -> super.getAttributeValue(index)
}
/**
* When attribute values are retrieved by name, pick up the synthetic id attribute at relative depth 0.
* Note that while the xml format does not use this method it is good practice to override it anyway.
*/
override fun getAttributeValue(nsUri: String?, localName: String): String? = when {
filterDepth == 0 &&
(nsUri ?: "") == idAttrName.namespaceURI &&
localName == idAttrName.localPart -> idValue
else -> super.getAttributeValue(nsUri, localName)
}
/**
* When we are at relative depth 0 we return the synthetic name rather than the original.
*/
override val namespaceURI: String
get() = when (filterDepth) {
0 -> elementName.namespaceURI
else -> super.namespaceURI
}
/**
* When we are at relative depth 0 we return the synthetic name rather than the original.
*/
override val localName: String
get() = when (filterDepth) {
0 -> elementName.localPart
else -> super.localName
}
/**
* When we are at relative depth 0 we return the synthetic name rather than the original.
*/
override val prefix: String
get() = when (filterDepth) {
0 -> elementName.prefix
else -> super.prefix
}
}/**
* This filter takes the writing of the proper tag and replaces it with writing the dynamic tag. It
* also ignores the writing of the id attribute. The id attribute is passed as parameter so it is not
* necessary to delay writing the tag until the id attribute has been written (which, while possible,
* introduces a lot of complexity which is unneeded in this case).
*/
internal class DynamicTagWriter(private val writer: XmlWriter, descriptor: XmlDescriptor, private val idValue: String) :
XmlDelegatingWriter(writer) {
private val initDepth = writer.depth
private val filterDepth: Int
/**
* We want to be safe so only handle the content at relative depth 0. The way that depth is determined
* means that the depth is the depth after the tag (and end tags are thus one level lower than the tag (and its
* content). We correct for that here.
*/
get() = writer.depth - initDepth
private val idAttrName = (0 until descriptor.elementsCount)
.first { descriptor.serialDescriptor.getElementName(it) == "id" }
.let { descriptor.getElementDescriptor(it) }
.tagName
/**
* When writing a start tag, if we are at relative depth 0 we write the dynamic tag instead of the
* default. Otherwise we just pass along the request directly to the parent.
*/
override fun startTag(namespace: String?, localName: String, prefix: String?) {
when (filterDepth) {
0 -> super.startTag("", "Test_$idValue", "")
else -> super.startTag(namespace, localName, prefix)
}
}
/**
* Once we have written the start tag we are at depth 1. In that case ignore the id attribute.
*/
override fun attribute(namespace: String?, name: String, prefix: String?, value: String) {
when {
filterDepth == 1 &&
(namespace ?: "") == idAttrName.namespaceURI &&
name == idAttrName.localPart
-> Unit
else -> super.attribute(namespace, name, prefix, value)
}
}
/**
* Also fix the end tag to actually write the dynamic name.
*/
override fun endTag(namespace: String?, localName: String, prefix: String?) {
when (filterDepth) {
1 -> super.endTag("", "Test_$idValue", "")
else -> super.endTag(namespace, localName, prefix)
}
}
}/**
* This example shows how a custom serializer together with a filter can be used to support non-xml xml documents
* where tag names are dynamic/unique. This example is a solution to the question in #41.
*
* There are 2 versions, one is the CompatContainerSerializer. This version works on 0.80.0 and 0.80.1 but has
* limitations in that it cannot inherit configuration or serializerModules. The improved version uses new properties
* in the XML.XmlInput and XML.XmlOutput interfaces that allow new xml serializers to be created based on the
* configuration of the encoder/decoder.
*/
fun main() {
/*
* Some test data that is used for both versions of the serializer.
*/
val testElements = listOf(
TestElement(123, 42, "someData"),
TestElement(456, 71, "moreData")
)
// Execute the example code for the compatible serializer
println("# Compatible")
compat(testElements)
// Execute the example code for the improved serializer
println()
println("# Improved version")
newExample(testElements)
}
private fun compat(testElements: List<TestElement>) {
val data = Container(testElements)
// Instead of using the serializer for the type we use the custom one. In normal cases there would only be one
// serializer
val serializer = CompatContainerSerializer
/*
* Set an indent here to show that it is not effective (as the serialization of the child does not have access to
* the configuration).
*/
val xml = XML { indent = 2 }
// Encode and print the output of serialization
val string = xml.encodeToString(serializer, data)
println("StringEncodingCompat:\n${string.prependIndent(" ")}")
// Parse and print the result of deserialization
val deserializedData = xml.decodeFromString(serializer, string)
println("Deserialized container:\n $deserializedData")
}
/** This example works with master, but not with the released version. */
private fun newExample(testElements: List<TestElement>) {
val data = Container(testElements)
val serializer = serializer<Container>() // use the default serializer
// Create the configuration for (de)serialization
val xml = XML { indent = 2 }
// Encode and print the output of serialization
val string = xml.encodeToString(serializer, data)
println("StringEncodingCompat:\n${string.prependIndent(" ")}")
// Parse and print the result of deserialization
val deserializedData = xml.decodeFromString(serializer, string)
println("Deserialized container:\n $deserializedData")
}