Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ public abstract class kotlinx/serialization/encoding/AbstractEncoder : kotlinx/s
public final fun encodeLongElement (Lkotlinx/serialization/descriptors/SerialDescriptor;IJ)V
public fun encodeNotNullMark ()V
public fun encodeNull ()V
public fun encodeNullableElement (Lkotlinx/serialization/descriptors/SerialDescriptor;IZ)Z
public fun encodeNullableSerializableElement (Lkotlinx/serialization/descriptors/SerialDescriptor;ILkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V
public fun encodeNullableSerializableValue (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V
public fun encodeSerializableElement (Lkotlinx/serialization/descriptors/SerialDescriptor;ILkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)V
Expand Down
1 change: 1 addition & 0 deletions core/api/kotlinx-serialization-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ abstract class kotlinx.serialization.encoding/AbstractEncoder : kotlinx.serializ
open fun encodeInt(kotlin/Int) // kotlinx.serialization.encoding/AbstractEncoder.encodeInt|encodeInt(kotlin.Int){}[0]
open fun encodeLong(kotlin/Long) // kotlinx.serialization.encoding/AbstractEncoder.encodeLong|encodeLong(kotlin.Long){}[0]
open fun encodeNull() // kotlinx.serialization.encoding/AbstractEncoder.encodeNull|encodeNull(){}[0]
open fun encodeNullableElement(kotlinx.serialization.descriptors/SerialDescriptor, kotlin/Int, kotlin/Boolean): kotlin/Boolean // kotlinx.serialization.encoding/AbstractEncoder.encodeNullableElement|encodeNullableElement(kotlinx.serialization.descriptors.SerialDescriptor;kotlin.Int;kotlin.Boolean){}[0]
open fun encodeShort(kotlin/Short) // kotlinx.serialization.encoding/AbstractEncoder.encodeShort|encodeShort(kotlin.Short){}[0]
open fun encodeString(kotlin/String) // kotlinx.serialization.encoding/AbstractEncoder.encodeString|encodeString(kotlin.String){}[0]
open fun encodeValue(kotlin/Any) // kotlinx.serialization.encoding/AbstractEncoder.encodeValue|encodeValue(kotlin.Any){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public abstract class AbstractEncoder : Encoder, CompositeEncoder {
*/
public open fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean = true

public open fun encodeNullableElement(descriptor: SerialDescriptor, index: Int, isNull: Boolean): Boolean = encodeElement(descriptor, index)

/**
* Invoked to encode a value when specialized `encode*` method was not overridden.
*/
Expand Down Expand Up @@ -86,7 +88,7 @@ public abstract class AbstractEncoder : Encoder, CompositeEncoder {
serializer: SerializationStrategy<T>,
value: T?
) {
if (encodeElement(descriptor, index))
if (encodeNullableElement(descriptor, index, value == null))
encodeNullableSerializableValue(serializer, value)
}
}
3 changes: 3 additions & 0 deletions formats/cbor/api/kotlinx-serialization-cbor.api
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public final class kotlinx/serialization/cbor/CborBuilder {
public final fun getIgnoreUnknownKeys ()Z
public final fun getPreferCborLabelsOverNames ()Z
public final fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
public final fun getUntaggedNullValueTags ()Z
public final fun getUseDefiniteLengthEncoding ()Z
public final fun getVerifyKeyTags ()Z
public final fun getVerifyObjectTags ()Z
Expand All @@ -46,6 +47,7 @@ public final class kotlinx/serialization/cbor/CborBuilder {
public final fun setIgnoreUnknownKeys (Z)V
public final fun setPreferCborLabelsOverNames (Z)V
public final fun setSerializersModule (Lkotlinx/serialization/modules/SerializersModule;)V
public final fun setUntaggedNullValueTags (Z)V
public final fun setUseDefiniteLengthEncoding (Z)V
public final fun setVerifyKeyTags (Z)V
public final fun setVerifyObjectTags (Z)V
Expand All @@ -60,6 +62,7 @@ public final class kotlinx/serialization/cbor/CborConfiguration {
public final fun getEncodeValueTags ()Z
public final fun getIgnoreUnknownKeys ()Z
public final fun getPreferCborLabelsOverNames ()Z
public final fun getUntaggedNullValueTags ()Z
public final fun getUseDefiniteLengthEncoding ()Z
public final fun getVerifyKeyTags ()Z
public final fun getVerifyObjectTags ()Z
Expand Down
5 changes: 5 additions & 0 deletions formats/cbor/api/kotlinx-serialization-cbor.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ final class kotlinx.serialization.cbor/CborBuilder { // kotlinx.serialization.cb
final var serializersModule // kotlinx.serialization.cbor/CborBuilder.serializersModule|{}serializersModule[0]
final fun <get-serializersModule>(): kotlinx.serialization.modules/SerializersModule // kotlinx.serialization.cbor/CborBuilder.serializersModule.<get-serializersModule>|<get-serializersModule>(){}[0]
final fun <set-serializersModule>(kotlinx.serialization.modules/SerializersModule) // kotlinx.serialization.cbor/CborBuilder.serializersModule.<set-serializersModule>|<set-serializersModule>(kotlinx.serialization.modules.SerializersModule){}[0]
final var untaggedNullValueTags // kotlinx.serialization.cbor/CborBuilder.untaggedNullValueTags|{}untaggedNullValueTags[0]
final fun <get-untaggedNullValueTags>(): kotlin/Boolean // kotlinx.serialization.cbor/CborBuilder.untaggedNullValueTags.<get-untaggedNullValueTags>|<get-untaggedNullValueTags>(){}[0]
final fun <set-untaggedNullValueTags>(kotlin/Boolean) // kotlinx.serialization.cbor/CborBuilder.untaggedNullValueTags.<set-untaggedNullValueTags>|<set-untaggedNullValueTags>(kotlin.Boolean){}[0]
final var useDefiniteLengthEncoding // kotlinx.serialization.cbor/CborBuilder.useDefiniteLengthEncoding|{}useDefiniteLengthEncoding[0]
final fun <get-useDefiniteLengthEncoding>(): kotlin/Boolean // kotlinx.serialization.cbor/CborBuilder.useDefiniteLengthEncoding.<get-useDefiniteLengthEncoding>|<get-useDefiniteLengthEncoding>(){}[0]
final fun <set-useDefiniteLengthEncoding>(kotlin/Boolean) // kotlinx.serialization.cbor/CborBuilder.useDefiniteLengthEncoding.<set-useDefiniteLengthEncoding>|<set-useDefiniteLengthEncoding>(kotlin.Boolean){}[0]
Expand Down Expand Up @@ -106,6 +109,8 @@ final class kotlinx.serialization.cbor/CborConfiguration { // kotlinx.serializat
final fun <get-ignoreUnknownKeys>(): kotlin/Boolean // kotlinx.serialization.cbor/CborConfiguration.ignoreUnknownKeys.<get-ignoreUnknownKeys>|<get-ignoreUnknownKeys>(){}[0]
final val preferCborLabelsOverNames // kotlinx.serialization.cbor/CborConfiguration.preferCborLabelsOverNames|{}preferCborLabelsOverNames[0]
final fun <get-preferCborLabelsOverNames>(): kotlin/Boolean // kotlinx.serialization.cbor/CborConfiguration.preferCborLabelsOverNames.<get-preferCborLabelsOverNames>|<get-preferCborLabelsOverNames>(){}[0]
final val untaggedNullValueTags // kotlinx.serialization.cbor/CborConfiguration.untaggedNullValueTags|{}untaggedNullValueTags[0]
final fun <get-untaggedNullValueTags>(): kotlin/Boolean // kotlinx.serialization.cbor/CborConfiguration.untaggedNullValueTags.<get-untaggedNullValueTags>|<get-untaggedNullValueTags>(){}[0]
final val useDefiniteLengthEncoding // kotlinx.serialization.cbor/CborConfiguration.useDefiniteLengthEncoding|{}useDefiniteLengthEncoding[0]
final fun <get-useDefiniteLengthEncoding>(): kotlin/Boolean // kotlinx.serialization.cbor/CborConfiguration.useDefiniteLengthEncoding.<get-useDefiniteLengthEncoding>|<get-useDefiniteLengthEncoding>(){}[0]
final val verifyKeyTags // kotlinx.serialization.cbor/CborConfiguration.verifyKeyTags|{}verifyKeyTags[0]
Expand Down
14 changes: 12 additions & 2 deletions formats/cbor/commonMain/src/kotlinx/serialization/cbor/Cbor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public sealed class Cbor(
verifyObjectTags = false,
useDefiniteLengthEncoding = false,
preferCborLabelsOverNames = false,
alwaysUseByteString = false
alwaysUseByteString = false,
untaggedNullValueTags = false,
), EmptySerializersModule()
) {

Expand Down Expand Up @@ -119,7 +120,8 @@ public fun Cbor(from: Cbor = Cbor, builderAction: CborBuilder.() -> Unit): Cbor
builder.verifyObjectTags,
builder.useDefiniteLengthEncoding,
builder.preferCborLabelsOverNames,
builder.alwaysUseByteString),
builder.alwaysUseByteString,
builder.untaggedNullValueTags),
builder.serializersModule
)
}
Expand Down Expand Up @@ -243,6 +245,14 @@ public class CborBuilder internal constructor(cbor: Cbor) {
*/
public var alwaysUseByteString: Boolean = cbor.configuration.alwaysUseByteString

/**
* Specifies whether [ValueTags] will be serialized for `null` values when [encodeValueTags] is enabled, and if
* such encodings pass validation when [verifyValueTags] is enabled. CBOR allows for untagged `null` values to
* reduce encoding size.
* See [RFC 8949 Tagging of Items](https://datatracker.ietf.org/doc/html/rfc8949#name-tagging-of-items) for more info.
*/
public var untaggedNullValueTags: Boolean = cbor.configuration.untaggedNullValueTags

/**
* Module with contextual and polymorphic serializers to be used in the resulting [Cbor] instance.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,14 @@ public class CborConfiguration internal constructor(
public val useDefiniteLengthEncoding: Boolean,
public val preferCborLabelsOverNames: Boolean,
public val alwaysUseByteString: Boolean,
public val untaggedNullValueTags: Boolean,
) {
override fun toString(): String {
return "CborConfiguration(encodeDefaults=$encodeDefaults, ignoreUnknownKeys=$ignoreUnknownKeys, " +
"encodeKeyTags=$encodeKeyTags, encodeValueTags=$encodeValueTags, encodeObjectTags=$encodeObjectTags, " +
"verifyKeyTags=$verifyKeyTags, verifyValueTags=$verifyValueTags, verifyObjectTags=$verifyObjectTags, " +
"useDefiniteLengthEncoding=$useDefiniteLengthEncoding, " +
"preferCborLabelsOverNames=$preferCborLabelsOverNames, alwaysUseByteString=$alwaysUseByteString)"
"preferCborLabelsOverNames=$preferCborLabelsOverNames, alwaysUseByteString=$alwaysUseByteString, " +
"untaggedNullValueTags=$untaggedNullValueTags)"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ internal open class CborReader(override val cbor: Cbor, protected val parser: Cb
override fun decodeInt() = parser.nextNumber(tags).toInt()
override fun decodeLong() = parser.nextNumber(tags)

override fun decodeNull() = parser.nextNull(tags)
override fun decodeNull() = parser.nextNull(tags, allowNoTags = cbor.configuration.untaggedNullValueTags)

override fun decodeEnum(enumDescriptor: SerialDescriptor): Int =
enumDescriptor.getElementIndexOrThrow(parser.nextString(tags))
Expand Down Expand Up @@ -172,8 +172,8 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO

fun isNull() = (curByte == NULL || curByte == EMPTY_MAP)

fun nextNull(tags: ULongArray? = null): Nothing? {
processTags(tags)
fun nextNull(tags: ULongArray? = null, allowNoTags: Boolean): Nothing? {
processTags(tags, allowNoTags)
if (curByte == NULL) {
skipByte(NULL)
} else if (curByte == EMPTY_MAP) {
Expand Down Expand Up @@ -250,7 +250,7 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO
input.readExactNBytes(strLen)
}

private fun processTags(tags: ULongArray?): ULongArray? {
private fun processTags(tags: ULongArray?, allowNoTags: Boolean = false): ULongArray? {
var index = 0
val collectedTags = mutableListOf<ULong>()
while ((curByte and 0b111_00000) == HEADER_TAG) {
Expand All @@ -265,6 +265,9 @@ internal class CborParser(private val input: ByteArrayInput, private val verifyO
}
readByte()
}
if (collectedTags.isEmpty() && allowNoTags) {
return null
}
return (if (collectedTags.isEmpty()) null else collectedTags.toULongArray()).also { collected ->
//We only want to compare if tags are actually set, otherwise, we don't care
tags?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ internal sealed class CborWriter(
}

override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean {
return encodeNullableElement(descriptor, index, false)
}


override fun encodeNullableElement(descriptor: SerialDescriptor, index: Int, isNull: Boolean): Boolean {
val destination = getDestination()
isClass = descriptor.getElementDescriptor(index).kind == StructureKind.CLASS
encodeByteArrayAsByteString = descriptor.isByteString(index)
Expand All @@ -137,7 +142,7 @@ internal sealed class CborWriter(
}
}

if (cbor.configuration.encodeValueTags) {
if (cbor.configuration.encodeValueTags && !(cbor.configuration.untaggedNullValueTags && isNull)) {
descriptor.getValueTags(index)?.forEach { destination.encodeTag(it) }
}
incrementChildren() // needed for definite len encoding, NOOP for indefinite length encoding
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2017-2025 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.cbor

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromHexString
import kotlinx.serialization.encodeToHexString
import kotlin.test.Test
import kotlin.test.assertEquals

@Serializable
data class NullableDataWithTags(
@ValueTags(12uL)
val a: ULong?,

@KeyTags(34uL)
val b: Int?,

@KeyTags(56uL)
@ValueTags(78uL)
@ByteString val c: ByteArray?,

@ValueTags(90uL, 12uL)
val d: String?
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as NullableDataWithTags

if (b != other.b) return false
if (a != other.a) return false
if (!c.contentEquals(other.c)) return false
if (d != other.d) return false

return true
}

override fun hashCode(): Int {
var result = b ?: 0
result = 31 * result + (a?.hashCode() ?: 0)
result = 31 * result + (c?.contentHashCode() ?: 0)
result = 31 * result + (d?.hashCode() ?: 0)
return result
}
}

class CborUntaggedNullTest {
@Test
fun encodeAndDecodeUntaggedNullValues() {
val cbor = Cbor {
encodeValueTags = true
verifyValueTags = true
encodeKeyTags = true
verifyKeyTags = true
untaggedNullValueTags = true
}

val o = NullableDataWithTags(
a = null,
b = null,
c = null,
d = null
)
val hex = cbor.encodeToHexString(o)
assertEquals("bf6161a0d8226162f6d8386163f66164f6ff", hex)

val decoded = cbor.decodeFromHexString<NullableDataWithTags>(hex)
assertEquals(o, decoded)
}
}