Package kotlinx.serialization.encoding

Basic concepts of encoding and decoding of serialized data.

Types

AbstractDecoder
Link copied to clipboard
common

A skeleton implementation of both Decoder and CompositeDecoder that can be used for simple formats and for testability purpose. Most of the decode* methods have default implementation that delegates decodeValue(value: Any) as TargetType. See Decoder documentation for information about each particular decode* method.

abstract class AbstractDecoder : Decoder, CompositeDecoder
AbstractEncoder
Link copied to clipboard
common

A skeleton implementation of both Encoder and CompositeEncoder that can be used for simple formats and for testability purpose. Most of the encode* methods have default implementation that delegates encodeValue(value: Any). See Encoder documentation for information about each particular encode* method.

abstract class AbstractEncoder : Encoder, CompositeEncoder
CompositeDecoder
Link copied to clipboard
common

CompositeDecoder is a part of decoding process that is bound to a particular structured part of the serialized form, described by the serial descriptor passed to Decoder.beginStructure.

Typically, for unordered data, CompositeDecoder is used by a serializer withing a decodeElementIndex-based loop that decodes all the required data one-by-one in any order and then terminates by calling endStructure. Please refer to decodeElementIndex for example of such loop.

All decode* methods have index and serialDescriptor parameters with a strict semantics and constraints:

  • descriptor argument is always the same as one used in Decoder.beginStructure.

  • index of the element being decoded. For sequential decoding, it is always a monotonic sequence from 0 to descriptor.elementsCount and for indexing-loop it is always an index that decodeElementIndex has returned from the last call.

The symmetric interface for the serialization process is CompositeEncoder.

Not stable for inheritance

CompositeDecoder interface is not stable for inheritance in 3rd party libraries, as new methods might be added to this interface or contracts of the existing methods can be changed.

interface CompositeDecoder
CompositeEncoder
Link copied to clipboard
common

CompositeEncoder is a part of encoding process that is bound to a particular structured part of the serialized form, described by the serial descriptor passed to Encoder.beginStructure.

All encode* methods have index and serialDescriptor parameters with a strict semantics and constraints:

  • descriptor is always the same as one used in Encoder.beginStructure. While this parameter may seem redundant, it is required for efficient serialization process to avoid excessive field spilling. If you are writing your own format, you can safely ignore this parameter and use one used in beginStructure for simplicity.

  • index of the element being encoded. This element at this index in the descriptor should be associated with the one being written.

The symmetric interface for the deserialization process is CompositeDecoder.

Not stable for inheritance

CompositeEncoder interface is not stable for inheritance in 3rd party libraries, as new methods might be added to this interface or contracts of the existing methods can be changed.

interface CompositeEncoder
Decoder
Link copied to clipboard
common

Decoder is a core deserialization primitive that encapsulates the knowledge of the underlying format and an underlying storage, exposing only structural methods to the deserializer, making it completely format-agnostic. Deserialization process takes a decoder and asks him for a sequence of primitive elements, defined by a deserializer serial form, while decoder knows how to retrieve these primitive elements from an actual format representations.

Decoder provides high-level API that operates with basic primitive types, collections and nested structures. Internally, the decoder represents input storage, and operates with its state and lower level format-specific details.

To be more specific, serialization asks a decoder for a sequence of "give me an int, give me a double, give me a list of strings and give me another object that is a nested int", while decoding transforms this sequence into a format-specific commands such as "parse the part of the string until the next quotation mark as an int to retrieve an int, parse everything within the next curly braces to retrieve elements of a nested object etc."

The symmetric interface for the serialization process is Encoder.

Deserialization. Primitives

If a class is represented as a single primitive value in its serialized form, then one of the decode* methods (e.g. decodeInt) can be used directly.

Deserialization. Structured types

If a class is represented as a structure or has multiple values in its serialized form, decode* methods are not that helpful, because format may not require a strict order of data (e.g. JSON or XML), do not allow working with collection types or establish structure boundaries. All these capabilities are delegated to the CompositeDecoder interface with a more specific API surface. To denote a structure start, beginStructure should be used.

// Denote the structure start,
val composite = decoder.beginStructure(descriptor)
// Decode all elements within the structure using 'composite'
...
// Denote the structure end
composite.endStructure(descriptor)

E.g. if the decoder belongs to JSON format, then beginStructure will parse an opening bracket ({ or [, depending on the descriptor kind), returning the CompositeDecoder that is aware of colon separator, that should be read after each key-value pair, whilst CompositeDecoder.endStructure will parse a closing bracket.

Exception guarantees.

For the regular exceptions, such as invalid input, missing control symbols or attributes and unknown symbols, SerializationException can be thrown by any decoder methods. It is recommended to declare a format-specific subclass of SerializationException and throw it.

Format encapsulation

For example, for the following deserializer:

class StringHolder(val stringValue: String)

object StringPairDeserializer : DeserializationStrategy<StringHolder> {
override val descriptor = ...

override fun deserializer(decoder: Decoder): StringHolder {
// Denotes start of the structure, StringHolder is not a "plain" data type
val composite = decoder.beginStructure(descriptor)
if (composite.decodeElementIndex(descriptor) != 0)
throw MissingFieldException("Field 'stringValue' is missing")
// Decode the nested string value
val value = composite.decodeStringElement(descriptor, index = 0)
// Denotes end of the structure
composite.endStructure(descriptor)
}
}

Exception safety

In general, catching SerializationException from any of decode* methods is not allowed and produces unspecified behaviour. After thrown exception, current decoder is left in an arbitrary state, no longer suitable for further decoding.

This deserializer does not know anything about the underlying data and will work with any properly-implemented decoder. JSON, for example, parses an opening bracket { during the beginStructure call, checks that the next key after this bracket is stringValue (using the descriptor), returns the value after the colon as string value and parses closing bracket } during the endStructure. XML would do roughly the same, but with different separators and parsing structures, while ProtoBuf machinery could be completely different. In any case, all these parsing details are encapsulated by a decoder.

Decoder implementation

While being strictly typed, an underlying format can transform actual types in the way it wants. For example, a format can support only string types and encode/decode all primitives in a string form:

StringFormatDecoder : Decoder {

...
override fun decodeDouble(): Double = decodeString().toDouble()
override fun decodeInt(): Int = decodeString().toInt()
...
}

Not stable for inheritance

Decoder interface is not stable for inheritance in 3rd party libraries, as new methods might be added to this interface or contracts of the existing methods can be changed.

interface Decoder
Encoder
Link copied to clipboard
common

Encoder is a core serialization primitive that encapsulates the knowledge of the underlying format and its storage, exposing only structural methods to the serializer, making it completely format-agnostic. Serialization process transforms a single value into the sequence of its primitive elements, also called its serial form, while encoding transforms these primitive elements into an actual format representation: JSON string, ProtoBuf ByteArray, in-memory map representation etc.

Encoder provides high-level API that operates with basic primitive types, collections and nested structures. Internally, encoder represents output storage and operates with its state and lower level format-specific details.

To be more specific, serialization transforms a value into a sequence of "here is an int, here is a double, here a list of strings and here is another object that is a nested int", while encoding transforms this sequence into a format-specific commands such as "insert opening curly bracket for a nested object start, insert a name of the value, and the value separated with colon for an int etc."

The symmetric interface for the deserialization process is Decoder.

Serialization. Primitives

If a class is represented as a single primitive value in its serialized form, then one of the encode* methods (e.g. encodeInt) can be used directly.

Serialization. Structured types.

If a class is represented as a structure or has multiple values in its serialized form, encode* methods are not that helpful, because they do not allow working with collection types or establish structure boundaries. All these capabilities are delegated to the CompositeEncoder interface with a more specific API surface. To denote a structure start, beginStructure should be used.

// Denote the structure start,
val composite = encoder.beginStructure(descriptor)
// Encoding all elements within the structure using 'composite'
...
// Denote the structure end
composite.endStructure(descriptor)

E.g. if the encoder belongs to JSON format, then beginStructure will write an opening bracket ({ or [, depending on the descriptor kind), returning the CompositeEncoder that is aware of colon separator, that should be appended between each key-value pair, whilst CompositeEncoder.endStructure will write a closing bracket.

Exception guarantees.

For the regular exceptions, such as invalid input, conflicting serial names, SerializationException can be thrown by any encoder methods. It is recommended to declare a format-specific subclass of SerializationException and throw it.

Format encapsulation

For example, for the following serializer:

class StringHolder(val stringValue: String)

object StringPairDeserializer : SerializationStrategy<StringHolder> {
override val descriptor = ...

override fun serializer(encoder: Encoder, value: StringHolder) {
// Denotes start of the structure, StringHolder is not a "plain" data type
val composite = encoder.beginStructure(descriptor)
// Encode the nested string value
composite.encodeStringElement(descriptor, index = 0)
// Denotes end of the structure
composite.endStructure(descriptor)
}
}

This serializer does not know anything about the underlying storage and will work with any properly-implemented encoder. JSON, for example, writes an opening bracket { during the beginStructure call, writes 'stringValuekey along with its value in encodeStringElementand writes the closing bracket }during the endStructure`. XML would do roughly the same, but with different separators and structures, while ProtoBuf machinery could be completely different. In any case, all these parsing details are encapsulated by an encoder.

Exception safety

In general, catching SerializationException from any of encode* methods is not allowed and produces unspecified behaviour. After thrown exception, current encoder is left in an arbitrary state, no longer suitable for further encoding.

Encoder implementation.

While being strictly typed, an underlying format can transform actual types in the way it wants. For example, a format can support only string types and encode/decode all primitives in a string form:

StringFormatEncoder : Encoder {

...
override fun encodeDouble(value: Double) = encodeString(value.toString())
override fun encodeInt(value: Int) = encodeString(value.toString())
...
}

Not stable for inheritance

Encoder interface is not stable for inheritance in 3rd party libraries, as new methods might be added to this interface or contracts of the existing methods can be changed.

interface Encoder

Functions

decodeStructure
Link copied to clipboard
common

Begins a structure, decodes it using the given block, ends it and returns decoded element.

inline fun <T> Decoder.decodeStructure(descriptor: SerialDescriptor, block: CompositeDecoder.() -> T): T
encodeStructure
Link copied to clipboard
common

Begins a structure, encodes it using the given block and ends it.

inline fun Encoder.encodeStructure(descriptor: SerialDescriptor, block: CompositeEncoder.() -> Unit)