Skip to content

Add Jackson IR extension for automatic annotation of generated models#674

Merged
wilmveel merged 5 commits into
masterfrom
claude/youthful-pasteur-h6scx5
Jun 19, 2026
Merged

Add Jackson IR extension for automatic annotation of generated models#674
wilmveel merged 5 commits into
masterfrom
claude/youthful-pasteur-h6scx5

Conversation

@wilmveel

Copy link
Copy Markdown
Contributor

Description

This PR adds a new JacksonExtension IR extension that automatically annotates generated Java and Kotlin models with Jackson serialization annotations, enabling polymorphic types to round-trip without manual mapper configuration.

The extension:

  • Adds @JsonProperty("<wirespec name>") to all record fields, preserving original field names even when sanitized for reserved keywords
  • Adds @JsonTypeInfo(... property = "type") to union types and @JsonTypeName("<wirespec name>") to union members, enabling polymorphic deserialization via a type discriminator
  • Uses fully qualified annotations (com.fasterxml.jackson.annotation.*) compatible with both Jackson 2 and 3
  • Generates syntax valid in both Java and Kotlin, keeping generated code dependency-free (only jackson-annotations runtime is required)
  • Only annotates top-level model declarations; endpoint-internal structs are left untouched

Implementation Details

The extension works by:

  1. Injecting RawElement annotation nodes directly before Field elements in struct parameter lists
  2. Injecting RawElement annotation nodes before union type declarations
  3. Leveraging the updated Struct.fields type (now List<Element> instead of List<Field>) to allow mixed field and annotation elements
  4. Updating Java and Kotlin code generators to handle RawElement annotations that precede fields

The Struct.fieldList helper property provides backward compatibility for code that needs only the actual field declarations.

Type of Change

  • Feature
  • Bug fix
  • Documentation update
  • Refactoring
  • Performance improvement
  • Build/CI pipeline changes
  • Other (please describe):

Checklist

  • I have followed the contribution guidelines
  • I have written tests for my changes
  • I have updated the documentation if necessary
  • I have written code in a functional style (using Arrow where appropriate)

Breaking Changes

The Struct.fields property type changed from List<Field> to List<Element> to support IR extensions that inject annotations. Code accessing struct fields should use the new Struct.fieldList helper property to filter out non-field elements. All existing code in the codebase has been updated to use this helper.

https://claude.ai/code/session_01AEWvdg9LmYam3uU4vCe2Ko

claude and others added 4 commits June 18, 2026 20:45
Introduce a JacksonExtension (mirroring KotlinxSerializationExtension) that
annotates generated Kotlin models during compilation:

- record fields get @JsonProperty("<wirespec name>")
- unions get @JsonTypeInfo + @JsonSubTypes mapping members to their wirespec
  names via a "type" discriminator

Annotations are emitted fully qualified against com.fasterxml.jackson.annotation
(shared by Jackson 2 and 3), keeping generated code dependency-free apart from
the jackson-annotations runtime.

To support per-field annotations, the IR Field node gains an `annotations`
list (defaulted empty) which the Kotlin generator renders on data-class
constructor parameters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEWvdg9LmYam3uU4vCe2Ko
Replaces the dedicated Field.annotations property (which mutated the IR AST)
with a structural change: Field now implements Element and Struct.fields is a
List<Element>. An IR extension can therefore inject a RawElement directly in
front of a field, and the Java/Kotlin generators render any raw element that
precedes a field as that field's annotation. A `Struct.fieldList` helper
returns just the declared fields.

JacksonExtension now annotates models with syntax valid in both Java and
Kotlin, so the same extension instance works on either emitter:

- record fields get @JsonProperty("<wirespec name>")
- unions get @JsonTypeInfo(... property = "type") and each member gets
  @JsonTypeName("<wirespec name>"); subtypes resolve from the sealed hierarchy

The non-JVM generators (Python, Rust, Scala, TypeScript) ignore non-field
elements in a struct's field list, preserving their output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEWvdg9LmYam3uU4vCe2Ko
Replace the repeated `fields.filterIsInstance<Field>()` calls across the
generators and emitters with the existing `Struct.fieldList` helper, and add a
`Struct.annotatedFields` helper that pairs each field with the raw annotation
codes preceding it. The Java and Kotlin generators now map over that helper
instead of duplicating an imperative accumulator loop.

Also simplify JacksonExtension's annotate dispatch to a smart-cast `when`,
computing each struct's pascalCase name once.

No change to generated output: emitter fixtures and tests are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEWvdg9LmYam3uU4vCe2Ko
* annotations. Elements that are neither a [Field] nor a [RawElement] are ignored.
*/
val Struct.annotatedFields: List<Pair<Field, List<String>>>
get() = buildList {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

val get? why not a function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — converted fieldList and annotatedFields to functions in cd1ebe9. They allocate a new list on each access (filtering / building), so a function reads more honestly than a property getter. All call sites updated.


Generated by Claude Code

when (element) {
is RawElement -> pending += element.code
is Field -> {
add(element to pending.toList())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what is happening here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This pairs each field with the annotations that precede it. An IR extension (e.g. Jackson) annotates a field by inserting RawElements directly in front of it in Struct.fields, so this walks the list in order, buffering each raw annotation code and attaching the buffer to the next Field. The Java/Kotlin generators then render those as the field's annotations. I've expanded the KDoc and renamed the accumulator to pendingAnnotations in cd1ebe9 to make that flow clearer.


Generated by Claude Code

"object $fileName {\n$staticContent def main(args: Array[String]): Unit = {\n$content }\n}\n\n".indentCode(indent)
}
is File -> elements.joinToString("") { it.emit(indent, isStatic, parents) }
is Field -> ""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why is a field just an empty string?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Since Field is now an Element, it has to be a branch in the top-level element when, but a field is never emitted on its own — it's rendered inline as part of its enclosing Struct's parameter list in Struct.emit. So reaching this branch standalone produces no output. Added a comment to that effect in each generator in cd1ebe9.


Generated by Claude Code

@jerrevanveluw jerrevanveluw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

i have no clue if this is ok.

Respond to review feedback on the Field-as-element helpers:

- Convert `Struct.fieldList` and `Struct.annotatedFields` from computed
  extension properties (which allocate on each access) to functions, and
  update all call sites.
- Expand the `annotatedFields` KDoc and rename its accumulator to explain how
  raw annotation elements are buffered and attached to the following field.
- Comment the `is Field -> ""` branch in each generator, noting a field has no
  standalone rendering and is emitted inline within its enclosing Struct.

No change to generated output: emitter fixtures and tests are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEWvdg9LmYam3uU4vCe2Ko
@wilmveel wilmveel merged commit e3a2437 into master Jun 19, 2026
34 checks passed
@wilmveel wilmveel deleted the claude/youthful-pasteur-h6scx5 branch June 19, 2026 17:47
@sonarqubecloud

Copy link
Copy Markdown

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.

3 participants