fix: emit an empty-list marker so query encoding round-trips empty lists - #143
Merged
Conversation
QueryEncoder previously emitted nothing for a list property with zero elements, which is indistinguishable on the wire from the property being absent. QueryDecoder then treated it as absent and fell back to the property's declared default, so encoding an explicitly-empty list against a non-empty default and decoding it back did not reproduce the empty list. Emit a name[] marker pair when a list encodes to zero elements, and have the decoder treat that marker as "present, zero elements" instead of falling back to the default. Non-empty lists are unaffected since their repeated name=value pairs already establish presence. Closes #86
…tlin/Native
Kotlin/Native rejects a comma in a backtick-quoted identifier ("Name
contains illegal characters"), so the empty-list round-trip test failed to
compile on the Apple/native targets even though the JVM test suite accepted
the name and passed. Reword the name to remove the comma; the behavior it
covers is unchanged.
decodeNotNullMark() only checked params.has(currentName), so a nullable List property carried solely by its tags[] empty-list marker (no tags=... pairs) looked absent to kotlinx's NullableSerializer, which short-circuited straight to null before ever reaching the QueryListDecoder that would have produced emptyList(). Track whether the current element matched via the marker and have decodeNotNullMark() report it as present too, so decoding continues into beginStructure as it already does for non-nullable lists.
…s collision KDoc beginCollection emitted the tags[] marker for any zero-size collection, while the decode side only recognizes it for List elements; align the encoder with a StructureKind.LIST guard so the two stay symmetric (the format doesn't support Map-typed properties today, so this is hardening rather than a live bug). Also corrects the marker's KDoc: it can't collide with a property's default serial name, but a property whose serial name is overridden via @SerialName to end in [] could still collide - that shape just isn't supported or tested.
This was referenced Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
QueryEncoder.beginCollectionemitted nothing at all for a list property with zero elements, which made "field present but empty" indistinguishable on the wire from "field absent."QueryDecoderthen treated the missing element as absent and fell back to the property's declared default, soencode(Foo(tags = emptyList()))followed bydecodesilently returned the non-empty default instead of an empty list.name[]marker pair whenever a list encodes to zero elements, and having the decoder recognize that marker (scoped to list-kind elements only) as "present, zero elements" instead of falling back to the default. Non-empty lists are untouched — their repeatedname=valuepairs already establish presence.Test plan
Foo(tags: List<String> = listOf("x")), encode withtags = emptyList(), decode, assertemptyList()notlistOf("x"))./gradlew :kuri-serde-kotlinx:jvmTest :kuri-serde-kotlinx:ktlintCheck :kuri-serde-kotlinx:detekt :kuri-serde-kotlinx:apiCheck— all green./gradlew :kuri-serde-kotlinx:jsNodeTest— green (commonMain touched)internal/private), so noapiDumpneeded — confirmed byapiCheckpassing unmodifiedCloses #86