Skip to content

Commit

Permalink
JSON omit null
Browse files Browse the repository at this point in the history
Resolves #195
  • Loading branch information
shanshin committed Jun 11, 2021
1 parent a106da3 commit 0d1d32b
Show file tree
Hide file tree
Showing 25 changed files with 678 additions and 129 deletions.
116 changes: 116 additions & 0 deletions benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/OmitNullBenchmark.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package kotlinx.benchmarks.json

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.openjdk.jmh.annotations.*
import java.util.concurrent.TimeUnit

@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@Fork(2)
open class OmitNullBenchmark {

@Serializable
data class Values(
val field0: Int?,
val field1: Int?,
val field2: Int?,
val field3: Int?,
val field4: Int?,
val field5: Int?,
val field6: Int?,
val field7: Int?,
val field8: Int?,
val field9: Int?,

val field10: Int?,
val field11: Int?,
val field12: Int?,
val field13: Int?,
val field14: Int?,
val field15: Int?,
val field16: Int?,
val field17: Int?,
val field18: Int?,
val field19: Int?,

val field20: Int?,
val field21: Int?,
val field22: Int?,
val field23: Int?,
val field24: Int?,
val field25: Int?,
val field26: Int?,
val field27: Int?,
val field28: Int?,
val field29: Int?,

val field30: Int?,
val field31: Int?
)


private val jsonOmitNull = Json { omitNull = true }

private val valueWithNulls = Values(
null, null, 2, null, null, null, null, null, null, null,
null, null, null, null, 14, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null,
null, null
)


private val jsonWithNulls = """{"field0":null,"field1":null,"field2":2,"field3":null,"field4":null,"field5":null,
|"field6":null,"field7":null,"field8":null,"field9":null,"field10":null,"field11":null,"field12":null,
|"field13":null,"field14":14,"field15":null,"field16":null,"field17":null,"field18":null,"field19":null,
|"field20":null,"field21":null,"field22":null,"field23":null,"field24":null,"field25":null,"field26":null,
|"field27":null,"field28":null,"field29":null,"field30":null,"field31":null}""".trimMargin()

private val jsonNoNulls = """{"field0":0,"field1":1,"field2":2,"field3":3,"field4":4,"field5":5,
|"field6":6,"field7":7,"field8":8,"field9":9,"field10":10,"field11":11,"field12":12,
|"field13":13,"field14":14,"field15":15,"field16":16,"field17":17,"field18":18,"field19":19,
|"field20":20,"field21":21,"field22":22,"field23":23,"field24":24,"field25":25,"field26":26,
|"field27":27,"field28":28,"field29":29,"field30":30,"field31":31}""".trimMargin()

private val jsonWithAbsence = """{"field2":2, "field14":14}"""

@Benchmark
fun decodeNoNulls() {
Json.decodeFromString<Values>(jsonNoNulls)
}

@Benchmark
fun decodeNoNullsWithOmit() {
jsonOmitNull.decodeFromString<Values>(jsonNoNulls)
}

@Benchmark
fun decodeNulls() {
Json.decodeFromString<Values>(jsonWithNulls)
}

@Benchmark
fun decodeNullsWithOmit() {
jsonOmitNull.decodeFromString<Values>(jsonWithNulls)
}

@Benchmark
fun decodeAbsenceWithOmit() {
jsonOmitNull.decodeFromString<Values>(jsonWithAbsence)
}

@Benchmark
fun encodeNulls() {
Json.encodeToString(valueWithNulls)
}

@Benchmark
fun encodeNullsWithOmit() {
jsonOmitNull.encodeToString(valueWithNulls)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ open class TwitterBenchmark {
private val input = TwitterBenchmark::class.java.getResource("/twitter.json").readBytes().decodeToString()
private val twitter = Json.decodeFromString(Twitter.serializer(), input)

private val jsonOmitNull = Json { omitNull = true }

@Setup
fun init() {
require(twitter == Json.decodeFromString(Twitter.serializer(), Json.encodeToString(Twitter.serializer(), twitter)))
Expand All @@ -34,6 +36,9 @@ open class TwitterBenchmark {
@Benchmark
fun decodeTwitter() = Json.decodeFromString(Twitter.serializer(), input)

@Benchmark
fun decodeTwitterOmitNull() = jsonOmitNull.decodeFromString(Twitter.serializer(), input)

@Benchmark
fun encodeTwitter() = Json.encodeToString(Twitter.serializer(), twitter)
}
9 changes: 9 additions & 0 deletions core/api/kotlinx-serialization-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ public abstract class kotlinx/serialization/encoding/AbstractEncoder : kotlinx/s
public fun encodeValue (Ljava/lang/Object;)V
public fun endStructure (Lkotlinx/serialization/descriptors/SerialDescriptor;)V
public fun shouldEncodeElementDefault (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
public fun skipNullElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
}

public abstract interface class kotlinx/serialization/encoding/CompositeDecoder {
Expand Down Expand Up @@ -628,6 +629,13 @@ public final class kotlinx/serialization/internal/DoubleSerializer : kotlinx/ser
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
}

public abstract class kotlinx/serialization/internal/ElementMarker {
public fun <init> (Lkotlinx/serialization/descriptors/SerialDescriptor;)V
protected abstract fun isPoppedElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
public final fun mark (I)V
public final fun popUnmarkedIndex ()I
}

public final class kotlinx/serialization/internal/EnumDescriptor : kotlinx/serialization/internal/PluginGeneratedSerialDescriptor {
public fun <init> (Ljava/lang/String;I)V
public fun equals (Ljava/lang/Object;)Z
Expand Down Expand Up @@ -1091,6 +1099,7 @@ public abstract class kotlinx/serialization/internal/TaggedEncoder : kotlinx/ser
protected final fun popTag ()Ljava/lang/Object;
protected final fun pushTag (Ljava/lang/Object;)V
public fun shouldEncodeElementDefault (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
protected fun skipNullElement (Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
}

public final class kotlinx/serialization/internal/TripleSerializer : kotlinx/serialization/KSerializer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public abstract class AbstractEncoder : Encoder, CompositeEncoder {
throw SerializationException("'null' is not supported by default")
}

public open fun skipNullElement(descriptor: SerialDescriptor, index: Int): Boolean {
return false
}

override fun encodeBoolean(value: Boolean): Unit = encodeValue(value)
override fun encodeByte(value: Byte): Unit = encodeValue(value)
override fun encodeShort(value: Short): Unit = encodeValue(value)
Expand Down Expand Up @@ -86,6 +90,10 @@ public abstract class AbstractEncoder : Encoder, CompositeEncoder {
serializer: SerializationStrategy<T>,
value: T?
) {
if (value == null && skipNullElement(descriptor, index)) {
return
}

if (encodeElement(descriptor, index))
encodeNullableSerializableValue(serializer, value)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package kotlinx.serialization.internal

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder

@InternalSerializationApi
@OptIn(ExperimentalSerializationApi::class)
public abstract class ElementMarker(private val descriptor: SerialDescriptor) {
/*
Element decoding marks from given bytes.
The element number is the same as the bit position.
Marks for the lowest 64 elements are always stored in a single Long value, higher elements stores in long array.
*/
private var lowerMarks: Long
private val highMarksArray: LongArray?

init {
val elementsCount = descriptor.elementsCount
if (elementsCount <= Long.SIZE_BITS) {
lowerMarks = if (elementsCount == Long.SIZE_BITS) {
// number of bits in the mark is equal to the number of fields
0L
} else {
// (1 - elementsCount) bits are always 1 since there are no fields for them
-1L shl elementsCount
}
highMarksArray = null
} else {
lowerMarks = 0L
// (elementsCount - 1) because only one Long value is needed to store 64 fields etc
val slotsCount = (elementsCount - 1) / Long.SIZE_BITS
val elementsInLastSlot = elementsCount % Long.SIZE_BITS
val highMarks = LongArray(slotsCount)
// (elementsCount % Long.SIZE_BITS) == 0 this means that the fields occupy all bits in mark
if (elementsInLastSlot != 0) {
// all marks except the higher are always 0
highMarks[highMarks.lastIndex] = -1L shl elementsCount
}
highMarksArray = highMarks
}
}

protected abstract fun isPoppedElement(descriptor: SerialDescriptor, index: Int): Boolean

public fun popUnmarkedIndex(): Int {
val elementsCount = descriptor.elementsCount
while (lowerMarks != -1L) {
val index = lowerMarks.inv().countTrailingZeroBits()
lowerMarks = lowerMarks or (1L shl index)

if (isPoppedElement(descriptor, index)) {
return index
}
}

if (elementsCount > Long.SIZE_BITS) {
val higherMarks = highMarksArray!!

for (slot in higherMarks.indices) {
// (slot + 1) because first element in high marks has index 64
val slotOffset = (slot + 1) * Long.SIZE_BITS
// store in a variable so as not to frequently use the array
var mark = higherMarks[slot]

while (mark != -1L) {
val indexInSlot = mark.inv().countTrailingZeroBits()
mark = mark or (1L shl indexInSlot)

val index = slotOffset + indexInSlot
if (isPoppedElement(descriptor, index)) {
higherMarks[slot] = mark
return index
}
}
higherMarks[slot] = mark
}
return CompositeDecoder.DECODE_DONE
}
return CompositeDecoder.DECODE_DONE
}

public fun mark(index: Int) {
if (index < Long.SIZE_BITS) {
lowerMarks = lowerMarks or (1L shl index)
} else {
val slot = (index / Long.SIZE_BITS) - 1
val offsetInSlot = index % Long.SIZE_BITS
highMarksArray!![slot] = highMarksArray[slot] or (1L shl offsetInSlot)
}
}
}
8 changes: 8 additions & 0 deletions core/commonMain/src/kotlinx/serialization/internal/Tagged.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ public abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {
return true
}

protected open fun skipNullElement(descriptor: SerialDescriptor, index: Int): Boolean {
return false
}

final override fun encodeNotNullMark() {} // Does nothing, open because is not really required
open override fun encodeNull(): Unit = encodeTaggedNull(popTag())
final override fun encodeBoolean(value: Boolean): Unit = encodeTaggedBoolean(popTag(), value)
Expand Down Expand Up @@ -143,6 +147,10 @@ public abstract class TaggedEncoder<Tag : Any?> : Encoder, CompositeEncoder {
serializer: SerializationStrategy<T>,
value: T?
) {
if (value == null && skipNullElement(descriptor, index)) {
return
}

if (encodeElement(descriptor, index))
encodeNullableSerializableValue(serializer, value)
}
Expand Down
Loading

0 comments on commit 0d1d32b

Please sign in to comment.