feat: nested sealed hierarchies for oneOf/anyOf subtypes#29
feat: nested sealed hierarchies for oneOf/anyOf subtypes#29halotukozak wants to merge 2 commits intofeat/12-unified-name-registryfrom
Conversation
…archies - ModelGenerator produces sealed class with nested data class subtypes for oneOf/anyOf-with-discriminator - Sealed interface pattern preserved only for anyOf-without-discriminator (JsonContentPolymorphicSerializer) - TypeMapping.toTypeName accepts classNameLookup for nested ClassName resolution - SerializersModuleGenerator uses parentClass.nestedClass(variant) for qualified references - One FileSpec per hierarchy, variant schemas filtered from separate file generation - All polymorphic tests updated for sealed class assertions, superclass checks, nested type verification Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…erator - ClientGenerator accepts classNameLookup for nested ClassName resolution in endpoint signatures - CodeGenerator builds classNameLookup via ModelGenerator companion and passes to ClientGenerator - All three TypeMapping.toTypeName call sites in ClientGenerator updated - Full test suite passes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the Kotlin model/code generation pipeline to emit polymorphic oneOf / discriminated anyOf schemas as sealed classes with nested subtypes (e.g., Shape.Circle) and wires a classNameLookup through generators so references resolve to nested ClassNames.
Changes:
- Generate sealed class hierarchies with nested data-class variants for
oneOf/ discriminatedanyOf(keepanyOfwithout discriminator as sealed interface +JsonContentPolymorphicSerializer). - Add
classNameLookup: Map<String, ClassName>support toTypeMapping.toTypeName()and propagate it throughCodeGenerator→ClientGenerator. - Update
SerializersModuleGeneratorto register nested variant classes and expand polymorphic generation tests accordingly.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ModelGenerator.kt | Implements sealed-class-with-nested-variants generation, builds classNameLookup, and adapts serializer generation paths. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/TypeMapping.kt | Extends type mapping API with classNameLookup for nested ClassName resolution. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/SerializersModuleGenerator.kt | Switches subclass registration to parentClass.nestedClass(variant). |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/CodeGenerator.kt | Computes and passes classNameLookup into ClientGenerator. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ClientGenerator.kt | Uses TypeMapping.toTypeName(..., classNameLookup) for params/return types to resolve nested models. |
| core/src/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorPolymorphicTest.kt | Updates/extends tests to assert nested sealed hierarchies and classNameLookup behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| is TypeRef.Array -> { | ||
| LIST.parameterizedBy(toTypeName(typeRef.items, modelPackage)) | ||
| } | ||
|
|
||
| is TypeRef.Map -> { | ||
| MAP.parameterizedBy(STRING, toTypeName(typeRef.valueType, modelPackage)) | ||
| } |
There was a problem hiding this comment.
TypeMapping.toTypeName now accepts classNameLookup, but the recursive calls for TypeRef.Array and TypeRef.Map don’t pass the lookup through. This will incorrectly map nested variant references like List<Circle> to the flat Circle instead of Shape.Circle. Propagate classNameLookup to the recursive toTypeName(...) calls in these branches.
| val variantClass = classNameLookup[variantName] ?: ClassName(modelPackage, variantName) | ||
| builder.addStatement( | ||
| "%S·in·element.%M -> %T.serializer()", | ||
| "%S\u00b7in\u00b7element.%M -> %T.serializer()", | ||
| uniqueField, | ||
| JSON_OBJECT_EXT, | ||
| ClassName(modelPackage, variantName), | ||
| variantClass, | ||
| ) |
There was a problem hiding this comment.
The generated selectDeserializer branches use a string template containing \u00b7 (middle-dot) between tokens ("%S\u00b7in\u00b7element..."). That will emit the · character into the generated Kotlin source ("field"·in·element...), which is not valid Kotlin syntax. Use normal spaces in the generated statement instead.
| } else { | ||
| // Empty variant with no properties — still need a constructor for data class | ||
| builder.primaryConstructor(FunSpec.constructorBuilder().build()) | ||
| } |
There was a problem hiding this comment.
When a variant schema can’t be found (variantSchema == null), the generator currently emits an empty nested data class and continues. That silently hides an invalid spec / missing component schema and can produce incorrect models. Prefer failing fast (throw) or emitting a structured generation error when a referenced variant schema is missing.
Summary
Shape.Circle,Shape.Squareinstead of separateCircle.kt,Square.ktclassNameLookup: Map<String, ClassName>toTypeMapping.toTypeName()for nested ClassName resolutionSerializersModuleGeneratorto useparentClass.nestedClass(variant)classNameLookupthroughCodeGenerator→ClientGeneratorDepends on: #27 (feat/12-unified-name-registry)
Test plan
Shape.Circle::class@JsonClassDiscriminator+@SerialNamepreserved🤖 Generated with Claude Code