Kotlin/Klaxon: fix deserialization of maps with empty-object values#2945
Merged
Conversation
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>
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.
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>withtypealias Reward = JsonObject), the generated code fails to parse valid input: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::classso empty objects deserialize at class-property positions. But Klaxon'sDefaultConverter.fromJsonObjectlooks up converters for map values withtypeValue.javaClassisjava.lang.Class— the class of the reflectionTypeobject 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 constructingJsonObjectreflectively, 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:@Target(AnnotationTarget.FIELD)annotationKlaxonJsonObjectMapplus a converter object, registers it via.fieldConverter(KlaxonJsonObjectMap::class, jsonObjectMapConverter), and annotates those properties.The converter's
fromJsonsimply returns the parsedJsonValue.obj— aJsonObjectwhose values are alreadyJsonObjects, i.e. exactly a runtimeMap<String, JsonObject>— and itstoJsondelegates toklaxon.toJsonString, so round-tripping is unchanged. Empty objects everywhere else (direct properties, arrays, top levels) keep the existingJsonObjecttypealias + converter path untouched.KotlinRenderer.renameAttributenow also receives theClassPropertyso 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:rewards: Map<String, Reward>;Map<String, Map<String, Empty>>properties are annotated too;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.jsonfrom the Kotlin/KlaxonskipJSONlist 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
klaxon-3.0.1.jarwith 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 exactKlaxonExceptionfrom the issue. Nested (Map<String, Map<String, Empty>>) and nullable (Map<String, Empty>?) shapes verified the same way.QUICKTEST=true FIXTURE=kotlin script/testpasses locally (all 44 tests, incl. the re-enabledbug2037.jsonwith compile + run + round-trip diff + schema diff).npm run build,npm run test:unit, andnpx biome checkon all changed files are clean.Fixes #2881
🤖 Generated with Claude Code