Package kotlinx.serialization.descriptors

Basic concepts of serial description to programmatically describe the serial form for serializers in an introspectable manner.

Types

ClassSerialDescriptorBuilder
Link copied to clipboard
common

Builder for SerialDescriptor for user-defined serializers.

Both explicit builder functions and implicit (using reified type-parameters) are present and are equivalent. For example, element<Int?>("nullableIntField") is indistinguishable from element("nullableIntField", IntSerializer.descriptor.nullable) and from element("nullableIntField", descriptor<Int?>).

Please refer to SerialDescriptor builder function for a complete example.

class ClassSerialDescriptorBuilder
PolymorphicKind
Link copied to clipboard
common

Polymorphic kind represents a (bounded) polymorphic value, that is referred by some base class or interface, but its structure is defined by one of the possible implementations. Polymorphic kind is, by its definition, a union kind and is extracted to its own subtype to emphasize bounded and sealed polymorphism common property: not knowing the actual type statically and requiring formats to additionally encode it.

sealed class PolymorphicKind : SerialKind
PrimitiveKind
Link copied to clipboard
common

Values of primitive kinds usually are represented as a single value. All default serializers for Kotlin primitives types and String have primitive kind.

Serializers interaction

Serialization formats typically handle these kinds by calling a corresponding primitive method on encoder or decoder. For example, if the following serializable class class Color(val red: Byte, val green: Byte, val blue: Byte) is represented by your serializer as a single Int value, a typical serializer will serialize its value in the following manner:

val intValue = color.rgbToInt()
encoder.encodeInt(intValue)

and a corresponding Decoder counterpart.

Implementation note

Serial descriptors for primitive kinds are not expected to have any nested elements, thus its element count should be zero. If a class is represented as a primitive value, its corresponding serial name should not be equal to the corresponding primitive type name. For the Color example, represented as single Int, its descriptor should have INT kind, zero elements and serial name not equals to kotlin.Int: PrimitiveDescriptor("my.package.ColorAsInt", PrimitiveKind.INT)

sealed class PrimitiveKind : SerialKind
SerialDescriptor
Link copied to clipboard
common

Serial descriptor is an inherent property of KSerializer that describes the structure of the serializable type. The structure of the serializable type is not only the property of the type, but also of the serializer as well, meaning that one type can have multiple descriptors that have completely different structure.

For example, the class class Color(val rgb: Int) can have multiple serializable representations, such as {"rgb": 255}, "#0000FF", [0, 0, 255] and {"red": 0, "green": 0, "blue": 255}. Representations are determined by serializers and each such serializer has its own descriptor that identifies each structure in a distinguishable and format-agnostic manner.

Structure

Serial descriptor is identified by its name and consists of kind, potentially empty set of children elements and additional metadata.

  • serialName uniquely identifies the descriptor (and the corresponding serializer) for non-generic types. For generic types, the actual type substitution is omitted from the string representation and the name identifies the family of the serializers without type substitutions. However, type substitution is accounted in equals and hashCode operations, meaning that descriptors of generic classes with the same name, but different type parameters, are not equal to each other. serialName is typically used to specify the type of the target class during serialization of polymorphic and sealed classes, for observability and diagnostics.

  • Kind defines what this descriptor represents: primitive, enum, object, collection et cetera.

  • Children elements are represented as serial descriptors as well and define the structure of the type's elements.

  • Metadata carries additional potentially useful information, such as nullability, optionality and serial annotations.

Usages

There are two general usages of the descriptors: THE serialization process and serialization introspection.

Serialization

Serial descriptor is used as bridge between decoders/encoders and serializers. When asking for a next element, the serializer provides an expected descriptor to the decoder, and, based on the descriptor content, decoder decides how to parse its input. In JSON, for example, when the encoder is asked to encode the next element and this element is a subtype of List, the encoder receives a descriptor with StructureKind.LIST and, based on that, first writes an opening square bracket before writing the content of the list.

Serial descriptor encapsulates the structure of the data, so serializers can be free from format-specific details. ListSerializer knows nothing about JSON and square brackets, providing only the structure of the data and delegating encoding decision to the format itself.

Introspection

Another usage of a serial descriptor is type introspection without its serialization. Introspection can be used to check, whether the given serializable class complies the corresponding scheme and to generate JSON or ProtoBuf schema from the given class.

Indices

Serial descriptor API operates with children indices. For the fixed-size structures, such as regular classes, index is represented by a value in the range from zero to elementsCount and represent and index of the property in this class. Consequently, primitives do not have children and their element count is zero.

For collections and maps, though, indices does not have fixed bound. Regular collections descriptors usually have one element (T, maps have two, one for keys and one for values), but potentially unlimited number of actual children values. Valid indices range is not known statically and implementations of descriptor should provide consistent and unbounded names and indices.

In practice, for regular classes it is allowed to invoke getElement*(index) methods with an index within 0 until elementsCount range and element at the particular index corresponds to the serializable property at the given position. For collections and maps, index parameter for getElement*(index) methods is effectively bound by the maximal number of collection/map elements.

Thread-safety and mutability

Serial descriptor implementation should be immutable and, thus, thread-safe.

Equality and caching

Serial descriptor can be used as a unique identifier for format-specific data or schemas and this implies the following restrictions on its equals and hashCode:

An equals implementation should use both serialName and elements structure. Comparing elementDescriptors directly is discouraged, because it may cause a stack overflow error, e.g. if a serializable class T contains elements of type T. To avoid it, a serial descriptor implementation should compare only descriptors of class' type parameters, in a way that serializer<Box<Int>>().descriptor != serializer<Box<String>>().descriptor. If type parameters are equal, descriptors structure should be compared by using children elements descriptors' serialNames, which correspond to class names (do not confuse with elements own names, which correspond to properties names); and/or other SerialDescriptor properties, such as kind. An example of equals implementation:

if (this === other) return true
if (other::class != this::class) return false
if (serialName != other.serialName) return false
if (!typeParametersAreEqual(other)) return false
if (this.elementDescriptors().map { it.serialName } != other.elementDescriptors().map { it.serialName }) return false
return true

hashCode implementation should use the same properties for computing the result.

User-defined serial descriptors

The best way to define a custom descriptor is to use SerialDescriptor builder function, where for each serializable property corresponding element is declared.

Example:

// Class with custom serializer and custom serial descriptor
class Data(
val intField: Int, // This field is ignored by custom serializer
val longField: Long, // This field is written as long, but in serialized form is named as "_longField"
val stringList: List<String> // This field is written as regular list of strings
)

// Descriptor for such class:
SerialDescriptor("my.package.Data") {
// intField is deliberately ignored by serializer -- not present in the descriptor as well
element<Long>("_longField") // longField is named as _longField
element("stringField", listDescriptor<String>())
}

For a classes that are represented as a single primitive value, PrimitiveSerialDescriptor builder function can be used instead.

Not stable for inheritance

SerialDescriptor 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. This interface is safe to build using buildClassSerialDescriptor and PrimitiveSerialDescriptor, and is safe to delegate implementation to existing instances.

interface SerialDescriptor
SerialKind
Link copied to clipboard
common

Serial kind is an intrinsic property of SerialDescriptor that indicates how the corresponding type is structurally represented by its serializer.

Kind is used by serialization formats to determine how exactly the given type should be serialized. For example, JSON format detects the kind of the value and, depending on that, may write it as a plain value for primitive kinds, open a curly brace '{' for class-like structures and square bracket '[' for list- and array- like structures.

Kinds are used both during serialization, to serialize a value properly and statically, and to introspect the type structure or build serialization schema.

Kind should match the structure of the serialized form, not the structure of the corresponding Kotlin class. Meaning that if serializable class class IntPair(val left: Int, val right: Int) is represented by the serializer as a single Long value, its descriptor should have PrimitiveKind.LONG without nested elements even though the class itself represents a structure with two primitive fields.

sealed class SerialKind
StructureKind
Link copied to clipboard
common

Structure kind represents values with composite structure of nested elements of depth and arbitrary number. We acknowledge following structured kinds:

Regular classes

The most common case for serialization, that represents an arbitrary structure with fixed count of elements. When the regular Kotlin class is marked as Serializable, its descriptor kind will be CLASS.

Lists

LIST represent a structure with potentially unknown in advance number of elements of the same type. All standard serializable List implementors and arrays are represented as LIST kind of the same type.

Maps

MAP represent a structure with potentially unknown in advance number of key-value pairs of the same type. All standard serializable Map implementors are represented as Map kind of the same type.

Kotlin objects

A singleton object defined with object keyword with an OBJECT kind. By default, objects are serialized as empty structures without any states and their identity is preserved across serialization within the same process, so you always have the same instance of the object.

Serializers interaction

Serialization formats typically handle these kinds by marking structure start and end. E.g. the following serializable class class IntHolder(myValue: Int) of structure kind CLASS is handled by serializer as the following call sequence:

val composite = encoder.beginStructure(descriptor) // Denotes the start of the structure
composite.encodeIntElement(descriptor, index = 0, holder.myValue)
composite.endStructure(descriptor) // Denotes the end of the structure

and its corresponding Decoder counterpart.

Serial descriptor implementors note

These kinds can be used not only for collection and regular classes. For example, provided serializer for Map.Entry represents it as Map type, so it is serialized as {"actualKey": "actualValue"} map directly instead of {"key": "actualKey", "value": "actualValue"}

sealed class StructureKind : SerialKind

Functions

buildClassSerialDescriptor
Link copied to clipboard
common

Builder for SerialDescriptor. The resulting descriptor will be uniquely identified by the given serialName, typeParameters and elements structure described in builderAction function.

Example:

// Class with custom serializer and custom serial descriptor
class Data(
val intField: Int, // This field is ignored by custom serializer
val longField: Long, // This field is written as long, but in serialized form is named as "_longField"
val stringList: List<String> // This field is written as regular list of strings
val nullableInt: Int?
)
// Descriptor for such class:
SerialDescriptor("my.package.Data") {
// intField is deliberately ignored by serializer -- not present in the descriptor as well
element<Long>("_longField") // longField is named as _longField
element("stringField", listDescriptor<String>())
element("nullableInt", descriptor<Int>().nullable)
}

Example for generic classes:

@Serializable(CustomSerializer::class)
class BoxedList<T>(val list: List<T>)

class CustomSerializer<T>(tSerializer: KSerializer<T>): KSerializer<BoxedList<T>> {
// here we use tSerializer.descriptor because it represents T
override val descriptor = SerialDescriptor("pkg.BoxedList", CLASS, typeParamSerializer.descriptor) {
// here we have to wrap it with List first, because property has type List<T>
element("list", ListSerializer(tSerializer).descriptor) // or listSerialDescriptor(tSerializer.descriptor)
}
}

fun buildClassSerialDescriptor(serialName: String, vararg typeParameters: Array<out SerialDescriptor>, builderAction: ClassSerialDescriptorBuilder.() -> Unit = {}): SerialDescriptor
buildSerialDescriptor
Link copied to clipboard
common

An unsafe alternative to buildClassSerialDescriptor that supports an arbitrary SerialKind. This function is left public only for migration of pre-release users and is not intended to be used as generally-safe and stable mechanism. Beware that it can produce inconsistent or non spec-compliant instances.

If you end up using this builder, please file an issue with your use-case in kotlinx.serialization

fun buildSerialDescriptor(serialName: String, kind: SerialKind, vararg typeParameters: Array<out SerialDescriptor>, builder: ClassSerialDescriptorBuilder.() -> Unit = {}): SerialDescriptor
element
Link copied to clipboard
common

A reified version of element function that extract descriptor using serializer<T>().descriptor call with all the restrictions of serializer<T>().descriptor.

inline fun <T> ClassSerialDescriptorBuilder.element(elementName: String, annotations: List<Annotation> = emptyList(), isOptional: Boolean = false)
getContextualDescriptor
Link copied to clipboard
common

Looks up a descriptor of serializer registered for contextual serialization in this, using SerialDescriptor.capturedKClass as a key.

fun SerializersModule.getContextualDescriptor(descriptor: SerialDescriptor): SerialDescriptor?
getPolymorphicDescriptors
Link copied to clipboard
common

Retrieves a collection of descriptors which serializers are registered for polymorphic serialization in this with base class equal to descriptor's SerialDescriptor.capturedKClass. This method does not retrieve serializers registered with PolymorphicModuleBuilder.default.

fun SerializersModule.getPolymorphicDescriptors(descriptor: SerialDescriptor): List<SerialDescriptor>
listSerialDescriptor
Link copied to clipboard
common

Creates a descriptor for the type List<T>.

inline fun <T> listSerialDescriptor(): SerialDescriptor

Creates a descriptor for the type List<T> where T is the type associated with elementDescriptor.

fun listSerialDescriptor(elementDescriptor: SerialDescriptor): SerialDescriptor
mapSerialDescriptor
Link copied to clipboard
common

Creates a descriptor for the type Map<K, V>.

inline fun <K, V> mapSerialDescriptor(): SerialDescriptor

Creates a descriptor for the type Map<K, V> where K and V are types associated with keyDescriptor and valueDescriptor respectively.

fun mapSerialDescriptor(keyDescriptor: SerialDescriptor, valueDescriptor: SerialDescriptor): SerialDescriptor
PrimitiveSerialDescriptor
Link copied to clipboard
common

Factory to create a trivial primitive descriptors. Primitive descriptors should be used when the serialized form of the data has a primitive form, for example:

object LongAsStringSerializer : KSerializer<Long> {
override val descriptor: SerialDescriptor =
PrimitiveDescriptor("kotlinx.serialization.LongAsStringSerializer", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: Long) {
encoder.encodeString(value.toString())
}

override fun deserialize(decoder: Decoder): Long {
return decoder.decodeString().toLong()
}
}

fun PrimitiveSerialDescriptor(serialName: String, kind: PrimitiveKind): SerialDescriptor
SerialDescriptor
Link copied to clipboard
common

Retrieves descriptor of type T using reified serializer function.

inline fun <T> serialDescriptor(): SerialDescriptor

Retrieves descriptor of type associated with the given KType

fun serialDescriptor(type: KType): SerialDescriptor
setSerialDescriptor
Link copied to clipboard
common

Creates a descriptor for the type Set<T>.

inline fun <T> setSerialDescriptor(): SerialDescriptor

Creates a descriptor for the type Set<T> where T is the type associated with elementDescriptor.

fun setSerialDescriptor(elementDescriptor: SerialDescriptor): SerialDescriptor

Properties

capturedKClass
Link copied to clipboard
common

Retrieves KClass associated with serializer and its descriptor, if it was captured.

For schema introspection purposes, capturedKClass can be used in SerializersModule as a key to retrieve registered descriptor at runtime. This property is intended to be used on SerialKind.CONTEXTUAL and PolymorphicKind.OPEN kinds of descriptors, where actual serializer used for a property can be determined only at runtime. Serializers which represent contextual serialization and open polymorphism (namely, ContextualSerializer and PolymorphicSerializer) capture statically known KClass in a descriptor and can expose it via this property.

This property is null for descriptors that are not of SerialKind.CONTEXTUAL or PolymorphicKind.OPEN kinds. It may be null for descriptors of these kinds, if captured class information is unavailable for various reasons. It means that schema introspection should be performed in an application-specific manner.

Example

Imagine we need to find all distinct properties names, which may occur in output after serializing a given class with respect to @Contextual annotation and all possible inheritors when the class is serialized polymorphically. Then we can write following function:

fun allDistinctNames(descriptor: SerialDescriptor, module: SerialModule) = when (descriptor.kind) {
is PolymorphicKind.OPEN -> module.getPolymorphicDescriptors(descriptor)
.map { it.elementNames() }.flatten().toSet()
is SerialKind.CONTEXTUAL -> module.getContextualDescriptor(descriptor)
?.elementNames().orEmpty().toSet()
else -> descriptor.elementNames().toSet()
}

val SerialDescriptor.capturedKClass: KClass<*>?
elementDescriptors
Link copied to clipboard
common

Returns an iterable of all descriptor elements.

val SerialDescriptor.elementDescriptors: Iterable<SerialDescriptor>
elementNames
Link copied to clipboard
common

Returns an iterable of all descriptor element names.

val SerialDescriptor.elementNames: Iterable<String>
nullable
Link copied to clipboard
common

Returns new serial descriptor for the same type with isNullable property set to true.

val SerialDescriptor.nullable: SerialDescriptor