Skip to content

Kotlin/Klaxon: fix deserialization of maps with empty-object values#2945

Merged
schani merged 2 commits into
masterfrom
fix/2881-klaxon-empty-object-maps
Jul 14, 2026
Merged

Kotlin/Klaxon: fix deserialization of maps with empty-object values#2945
schani merged 2 commits into
masterfrom
fix/2881-klaxon-empty-object-maps

Conversation

@schani

@schani schani commented Jul 14, 2026

Copy link
Copy Markdown
Member

Problem

The Kotlin/Klaxon renderer renders empty object types as typealias X = JsonObject. When such a type appears as a map value (val rewards: Map<String, Reward> with typealias Reward = JsonObject), the generated code fails to parse valid input:

KlaxonException: Couldn't find a suitable constructor for class JsonObject to initialize with {}

Minimal repro (from #2877's fixture, test/inputs/json/priority/bug2037.json):

{"mission_specs": {"1": {"objectives": {"2": {"rewards": {"3": {}, "5": {}}}}}, "4": {"objectives": {"6": {"rewards": {}}}}}}

Root cause

quicktype already registers a custom converter for JsonObject::class so empty objects deserialize at class-property positions. But Klaxon's DefaultConverter.fromJsonObject looks up converters for map values with

val typeValue = jt.actualTypeArguments[1]
val converter = klaxon.findConverterFromClass(typeValue.javaClass, null)

typeValue.javaClass is java.lang.Class — the class of the reflection Type object itself, not the value type — so custom converters are never consulted for map values (present in Klaxon 3.0.1 through current master). Klaxon then falls back to constructing JsonObject reflectively, and its only constructor (JsonObject(map: MutableMap<String, Any?>)) can't be satisfied from {}.

Fix

Klaxon does consult field-level converters (Klaxon.fieldConverter), which receive the whole field value before the broken map-value lookup is reached. The Klaxon renderer now:

  • detects class properties whose type is a map (directly, or nested in further maps, incl. nullable ones) whose values are empty-object types;
  • when any exist, declares a @Target(AnnotationTarget.FIELD) annotation KlaxonJsonObjectMap plus a converter object, registers it via .fieldConverter(KlaxonJsonObjectMap::class, jsonObjectMapConverter), and annotates those properties.

The converter's fromJson simply returns the parsed JsonValue.obj — a JsonObject whose values are already JsonObjects, i.e. exactly a runtime Map<String, JsonObject> — and its toJson delegates to klaxon.toJsonString, so round-tripping is unchanged. Empty objects everywhere else (direct properties, arrays, top levels) keep the existing JsonObject typealias + converter path untouched.

KotlinRenderer.renameAttribute now also receives the ClassProperty so the Klaxon renderer can inspect the property type when emitting attribute annotations (other Kotlin renderers are unaffected).

Tests (written first)

  • New unit test test/unit/kotlin-klaxon-empty-object-map.test.ts:

    • the issue's repro must produce the field-converter declaration/registration and annotate rewards: Map<String, Reward>;
    • nested Map<String, Map<String, Empty>> properties are annotated too;
    • maps of non-empty classes get none of the workaround machinery.

    These were committed first (80a0aec) and fail without the fix (2 failed / 1 passed); with the fix all pass (npm run test:unit: 74/74 green).

  • Fixture re-enabled: removed bug2037.json from the Kotlin/Klaxon skipJSON list that Fix "Type creator didn't return its forwarding ref" crash on sibling maps #2877 had to add, so the priority fixture now guards this end-to-end in CI.

Verification

  • Runtime verified against the pinned klaxon-3.0.1.jar with a locally downloaded kotlinc: the exact issue scenario now parses and round-trips ({"mission_specs" : {"1": {...{"3":{},"5":{}}...}, "4": {...{}}}}), where the pre-fix code reproduced the exact KlaxonException from the issue. Nested (Map<String, Map<String, Empty>>) and nullable (Map<String, Empty>?) shapes verified the same way.
  • QUICKTEST=true FIXTURE=kotlin script/test passes locally (all 44 tests, incl. the re-enabled bug2037.json with compile + run + round-trip diff + schema diff).
  • npm run build, npm run test:unit, and npx biome check on all changed files are clean.

Fixes #2881

🤖 Generated with Claude Code

schani and others added 2 commits July 14, 2026 13:48
Maps of empty objects render as Map<String, X> with typealias X =
JsonObject, but Klaxon's reflective deserializer never consults custom
converters for map values, so parsing fails with "Couldn't find a
suitable constructor for class JsonObject to initialize with {}".

Adds unit tests expecting such fields to be handled by a field-level
converter, and re-enables the bug2037.json fixture for Kotlin/Klaxon
that #2877 had to skip. The unit tests currently fail; the fix follows
in the next commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Empty object types render as `typealias X = JsonObject`, and a custom
converter registered for JsonObject::class handles them. But Klaxon's
DefaultConverter looks up converters for map values with
`findConverterFromClass(typeValue.javaClass, ...)`, where
`typeValue.javaClass` is `java.lang.Class` rather than the value type,
so custom converters are never consulted for map values. Klaxon then
falls back to constructing JsonObject reflectively and fails with
"Couldn't find a suitable constructor for class JsonObject to
initialize with {}".

Klaxon does consult field-level converters, so properties whose type
is a map (possibly nested in further maps) with empty-object values
now get a generated `@KlaxonJsonObjectMap` field annotation, backed by
a registered field converter that returns the parsed JsonObject (which
already is a `Map<String, JsonObject>` at runtime) as-is.

Fixes #2881

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@schani
schani merged commit a09c2be into master Jul 14, 2026
24 checks passed
@schani
schani deleted the fix/2881-klaxon-empty-object-maps branch July 14, 2026 18:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kotlin Klaxon cannot deserialize empty object map values as JsonObject

1 participant