Add Jackson IR extension for automatic annotation of generated models#674
Conversation
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 { |
There was a problem hiding this comment.
val get? why not a function?
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
what is happening here?
There was a problem hiding this comment.
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 -> "" |
There was a problem hiding this comment.
why is a field just an empty string?
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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
|



Description
This PR adds a new
JacksonExtensionIR 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:
@JsonProperty("<wirespec name>")to all record fields, preserving original field names even when sanitized for reserved keywords@JsonTypeInfo(... property = "type")to union types and@JsonTypeName("<wirespec name>")to union members, enabling polymorphic deserialization via atypediscriminatorcom.fasterxml.jackson.annotation.*) compatible with both Jackson 2 and 3Implementation Details
The extension works by:
RawElementannotation nodes directly beforeFieldelements in struct parameter listsRawElementannotation nodes before union type declarationsStruct.fieldstype (nowList<Element>instead ofList<Field>) to allow mixed field and annotation elementsRawElementannotations that precede fieldsThe
Struct.fieldListhelper property provides backward compatibility for code that needs only the actual field declarations.Type of Change
Checklist
Breaking Changes
The
Struct.fieldsproperty type changed fromList<Field>toList<Element>to support IR extensions that inject annotations. Code accessing struct fields should use the newStruct.fieldListhelper 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