feat: nested sealed hierarchies for oneOf/anyOf subtypes#28
feat: nested sealed hierarchies for oneOf/anyOf subtypes#28halotukozak 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 - TypeMapping.toTypeName accepts classNameLookup for nested ClassName resolution - SerializersModuleGenerator uses parentClass.nestedClass(variant) for variant references - anyOf-without-discriminator path unchanged (sealed interface + JsonContentPolymorphicSerializer) - One FileSpec per hierarchy, variant schemas skipped from top-level generation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…erator - ClientGenerator accepts classNameLookup for nested ClassName resolution - All TypeMapping.toTypeName calls in ClientGenerator pass classNameLookup - CodeGenerator builds classNameLookup via ModelGenerator.buildClassNameLookup companion - 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 strategy for polymorphic schemas so that oneOf and anyOf with discriminator generate a sealed class with nested data-class variants (e.g., Shape.Circle, Shape.Square), and wires nested ClassName resolution through TypeMapping into the client generator and serializers module generation.
Changes:
- Generate polymorphic
oneOf/ discriminatedanyOfas sealed classes with nested variant data classes, producing one file per hierarchy. - Extend
TypeMapping.toTypeNamewith an optionalclassNameLookupto resolve references to nested types. - Update serializers module generation and client generation to reference nested variants (e.g.,
Shape.Circle::class) via the lookup.
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/test/kotlin/com/avsystem/justworks/core/gen/ModelGeneratorPolymorphicTest.kt | Updates/expands tests to assert nested sealed hierarchies and nested serializer references. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/TypeMapping.kt | Adds classNameLookup support for resolving TypeRef.Reference to nested ClassNames. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/SerializersModuleGenerator.kt | Switches variant registration from flat ClassName to parentClass.nestedClass(variant). |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ModelGenerator.kt | Implements sealed class + nested variants generation; filters out separate variant files; adds lookup builder. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/CodeGenerator.kt | Builds and passes the classNameLookup into ClientGenerator. |
| core/src/main/kotlin/com/avsystem/justworks/core/gen/ClientGenerator.kt | Uses classNameLookup when resolving endpoint parameter and response types. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| for (variantName in variants) { | ||
| val variantSchema = schemasById[variantName] ?: continue |
There was a problem hiding this comment.
generateSealedHierarchy silently skips a variant when the referenced variant schema isn't present (schemasById[variantName] ?: continue). That can produce an incomplete sealed hierarchy without any signal to the caller. Consider failing fast (e.g., requireNotNull) or surfacing a structured error/warning so missing/typoed variant refs don't generate broken output.
| val variantSchema = schemasById[variantName] ?: continue | |
| val variantSchema = requireNotNull(schemasById[variantName]) { | |
| "Missing schema for sealed hierarchy variant '$variantName' in '${schema.name}'" | |
| } |
| val constructorBuilder = FunSpec.constructorBuilder() | ||
| val propertySpecs = sortedProps.map { prop -> | ||
| val type = TypeMapping.toTypeName(prop.type, modelPackage).copy(nullable = prop.nullable) | ||
| val kotlinName = prop.name.toCamelCase() | ||
|
|
||
| val paramBuilder = ParameterSpec.builder(kotlinName, type) | ||
|
|
There was a problem hiding this comment.
Nested variant properties currently resolve referenced schemas via TypeMapping.toTypeName(prop.type, modelPackage) without passing the new classNameLookup. If a nested variant (or its inline schemas) references another variant schema by name, this will emit a flat ClassName(modelPackage, "Variant") that no longer exists as a top-level type after nesting. Consider computing the lookup once from HierarchyInfo and threading it into these toTypeName calls.
| val variantNames = hierarchyInfo.sealedHierarchies | ||
| .filterKeys { it !in hierarchyInfo.anyOfWithoutDiscriminator } | ||
| .values | ||
| .flatten() | ||
| .toSet() | ||
|
|
||
| val schemaFiles = spec.schemas | ||
| .filter { it.name !in variantNames } | ||
| .flatMap { generateSchemaFiles(it) } | ||
|
|
||
| val inlineSchemaFiles = collectAllInlineSchemas(spec).map { | ||
| if (it.isNested) generateNestedInlineClass(it) else generateDataClass(it) | ||
| } |
There was a problem hiding this comment.
generate() now filters out variant schemas from schemaFiles (so Circle.kt/Square.kt etc are no longer generated for discriminated hierarchies). After this change, any remaining TypeRef.Reference("Circle") usage outside the parent hierarchy (e.g., in other schema properties or inline schemas) must resolve to the nested type (Shape.Circle) or the generated model code will reference a missing top-level class. Consider building a classNameLookup from hierarchyInfo and using it consistently inside ModelGenerator (not only in ClientGenerator).
|
Closing — will re-create on top of updated feat/12-unified-name-registry branch to resolve conflicts. |
Summary
Shape.Circle,Shape.Squareinstead of separateCircle.kt,Square.ktclassNameLookup: Map<String, ClassName>toTypeMapping.toTypeName()for nested ClassName resolutionSerializersModuleGeneratorto useparentClass.nestedClass(variant)classNameLookupthroughCodeGenerator→ClientGeneratorfor endpoint type resolutionDepends on: #27 (feat/12-unified-name-registry)
Test plan
ModelGeneratorPolymorphicTest— nested sealed class assertions, one file per hierarchy, no separate variant filesShape.Circle::classreferences@JsonClassDiscriminatoron parent,@SerialNameon nested subtypes preserved🤖 Generated with Claude Code