Skip to content

feat: nested sealed hierarchies for oneOf/anyOf subtypes#29

Open
halotukozak wants to merge 2 commits intofeat/12-unified-name-registryfrom
feat/nested-sealed-hierarchies
Open

feat: nested sealed hierarchies for oneOf/anyOf subtypes#29
halotukozak wants to merge 2 commits intofeat/12-unified-name-registryfrom
feat/nested-sealed-hierarchies

Conversation

@halotukozak
Copy link
Member

Summary

  • Restructure polymorphic generation from sealed interfaces + separate files to sealed classes with nested data class subtypes
  • Shape.Circle, Shape.Square instead of separate Circle.kt, Square.kt
  • Add classNameLookup: Map<String, ClassName> to TypeMapping.toTypeName() for nested ClassName resolution
  • Update SerializersModuleGenerator to use parentClass.nestedClass(variant)
  • Wire classNameLookup through CodeGeneratorClientGenerator
  • anyOf-without-discriminator stays as sealed interface

Depends on: #27 (feat/12-unified-name-registry)

Test plan

  • Nested sealed class assertions, one file per hierarchy
  • SerializersModule uses qualified Shape.Circle::class
  • @JsonClassDiscriminator + @SerialName preserved
  • ClientGenerator resolves variant types via classNameLookup
  • Full test suite passing

🤖 Generated with Claude Code

halotukozak and others added 2 commits March 24, 2026 11:30
…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>
Copilot AI review requested due to automatic review settings March 24, 2026 10:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / discriminated anyOf (keep anyOf without discriminator as sealed interface + JsonContentPolymorphicSerializer).
  • Add classNameLookup: Map<String, ClassName> support to TypeMapping.toTypeName() and propagate it through CodeGeneratorClientGenerator.
  • Update SerializersModuleGenerator to 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.

Comment on lines 42 to 48
is TypeRef.Array -> {
LIST.parameterizedBy(toTypeName(typeRef.items, modelPackage))
}

is TypeRef.Map -> {
MAP.parameterizedBy(STRING, toTypeName(typeRef.valueType, modelPackage))
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +400 to 406
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,
)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +298 to +301
} else {
// Empty variant with no properties — still need a constructor for data class
builder.primaryConstructor(FunSpec.constructorBuilder().build())
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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.

2 participants