Skip to content

[Codegen: Java & C#] Support traditional mutable classes/POJOs with getters, setters, and builders (--style pojo / --builder) #46

Closed

Description

@nth-bailey

🎯 Motivation & Background

Currently, PolyXML defaults to modern immutable data representations across all ecosystems—specifically Java 21+ record and C# 12 primary constructor record types.

While modern, immutable records provide excellent thread-safety and pattern-matching ergonomics, enterprise engineering teams often face severe friction in real-world production environments when integrating with existing enterprise stacks:

  1. Legacy ORM & Framework Compatibility (The Spring/Hibernate Problem):

    • Enterprise Java frameworks (Hibernate / JPA, MyBatis, Spring Data, ModelMapper, Dozer, MapStruct, Apache Commons BeanUtils) require a parameterless default constructor (new EntityMt()) and standard JavaBean getters/setters (getId(), setId(...)).
    • Java record types only provide canonical constructors and component accessor methods named id() instead of getId(), which breaks older reflection-based bean introspection and dynamic proxying.
  2. Constructor Parameter Explosion on Massive Schemas:

    • Complex enterprise schemas—such as USAF UCI (EntityMt), ISO 20022 (pacs.008), and CEN NeTEx—routinely feature 40 to 100+ fields, the majority of which are optional.
    • With an immutable record, instantiating an instance manually requires passing 80+ positional arguments:
      // Unwieldy and error-prone with 50-80 parameters:
      var entity = new EntityMt(id, null, null, null, null, timestamp, null, ...);
    • With a traditional class or builder pattern, instantiation is clean and ergonomic:
      var entity = new EntityMt();
      entity.setId("UUID-1234");
      entity.setTimestamp(Instant.now());
  3. In-Place Pipeline Mutation:

    • In telematics gateways, message routers, and financial settlement switches, an application often receives an XML message, updates 1 or 2 fields (e.g. status, processedAt), and re-serializes it downstream. Immutability forces copying the entire object hierarchy on every step, causing unnecessary garbage collection churn.
  4. Drop-in JAXB (xjc) & xsd.exe Migration:

    • Enterprises with 10- to 20-year-old codebases written against xjc-generated classes often have thousands of lines of code expecting order.getItems().add(...) or entity.setStatus(...). Providing traditional classes enables zero-refactoring, drop-in replacement of JAXB.

🚀 Modern POJOs vs. Legacy JAXB (xjc): Architectural Advantages

While --style pojo provides drop-in compatibility for enterprise codebases expecting JavaBeans, PolyXML does not simply replicate 2003-era JAXB (xjc) output. It modernizes the POJO paradigm with key architectural improvements:

Feature Legacy JAXB (xjc) PolyXML Modern POJO (--style pojo)
Target Java Version Java 5 / 6 / 8 era Java 17 / 21+
Dates & Times Archaic XMLGregorianCalendar Modern java.time.* (Instant, LocalDate, OffsetDateTime)
Wrapper Bloat JAXBElement<T> & massive ObjectFactory.java Clean idiomatic types, direct fields, or standard Optional<T>
JSON Support ❌ None (Forces duplicate DTOs + manual mappers) ✅ Native dual-serialization (Jackson @JsonProperty & @JacksonXmlProperty)
Facet Validation ❌ Discarded in generated code ✅ Jakarta Validation annotations (@NotNull, @Size, @Pattern, @Min, @Max)
Construction Empty constructor + 50 setters Fluent, type-safe .builder() pattern (--builder)
Performance Engine Heavy JVM reflection (JAXBContext.newInstance) Jackson, Direct Compile-Time Codecs, or Zero-Copy Panama FFI streaming
External Dependencies Heavy (jaxb-api / jaxb-impl classpath conflicts) Zero required runtime jars (or standard Jackson in Spring Boot)

Detailed Advantages:

  1. Modern Date/Time Types (java.time.*) vs. XMLGregorianCalendar:

    • JAXB generates archaic XMLGregorianCalendar and javax.xml.datatype.Duration (Java 5 era), which are notoriously cumbersome, thread-unsafe, and require manual conversion helpers.
    • PolyXML generates standard Java 8+ JSR-310 types directly: Instant, OffsetDateTime, LocalDate, and LocalTime.
  2. Elimination of JAXBElement<T> & ObjectFactory Hell:

    • For nillable, substitution, or optional choice elements, JAXB wraps fields in cumbersome JAXBElement<T> wrappers (e.g. order.getDetails().getValue().getCustomerRef().getValue()) and generates multi-thousand-line ObjectFactory.java boilerplate files.
    • PolyXML generates clean, idiomatic Java types with direct nullable fields or standard Optional<T> accessors—zero JAXBElement<T> wrappers and zero ObjectFactory clutter.
  3. Dual-Format Serialization (Single DTO for XML + JSON):

    • JAXB is strictly XML-only. In modern microservice architectures, developers must maintain duplicate DTO classes or write complex MapStruct mappers to convert between JAXB XML DTOs and Jackson/REST JSON DTOs.
    • PolyXML POJOs support native dual annotations (@JsonProperty and @JacksonXmlProperty), allowing the exact same POJO to serialize and deserialize to both XML and JSON seamlessly.
  4. XSD Facet Enforcement via Jakarta Validation:

    • JAXB completely discards XSD facets (pattern, minLength, maxLength, minInclusive, maxInclusive) in generated Java code, requiring an external Xerces XML validator.
    • PolyXML converts XSD facets directly into standard Jakarta Validation annotations (@NotNull, @Size(min=..., max=...), @Pattern(regexp=...), @Min, @Max), enabling automatic validation at Spring @Valid controller and service boundaries.
  5. Type-Safe Fluent Builders (--builder):

    • In massive 50–100+ field enterprise schemas (ISO 20022, UCI, FIXML), JAXB leaves developers with an empty constructor and 80 setters. PolyXML generates type-safe fluent builders (EntityMt.builder().id(...).build()).
  6. Escape from Reflection Bottlenecks via Project Panama:

    • JAXB depends on heavy runtime reflection (JAXBContext.newInstance(...)), which causes slow cold starts and high heap churn. PolyXML offers native Rust-powered streaming codecs accessible via Project Panama (Java 22+ FFI JEP 454), running 10x–20x faster with zero reflection.
  7. Zero Jakarta/JAXB Dependency Clashes:

    • JAXB was removed from the JDK in Java 11, creating constant classpath conflicts (javax.xml.bind vs jakarta.xml.bind vs jaxb-runtime). PolyXML POJOs run cleanly with standard Java libraries or standard Jackson already present in modern Spring/Quarkus/Micronaut apps.

⚡ High-Performance Modern Java Enhancements (Serialization & Deserialization)

Beyond syntax ergonomics, we can exploit modern Java runtime features to achieve industry-leading serialization/deserialization throughput:

1. Compile-Time Companion Codecs (--codec direct)

  • The Problem with Reflection: Traditional libraries (JAXB, standard Jackson, Hibernate) use runtime reflection (Field.get(), Method.invoke()) to inspect annotations and read/write fields. This incurs dynamic method dispatch checks, boxing overhead, and prevents HotSpot C2 compiler inlining.
  • The Solution: PolyXML can generate a companion compile-time serializer/deserializer (EntityMtCodec.java):
    public final class EntityMtCodec {
        public static void writeXml(EntityMt entity, XMLStreamWriter writer) throws XMLStreamException {
            writer.writeStartElement("EntityMt");
            if (entity.getId() != null) {
                writer.writeAttribute("id", entity.getId()); // Direct getter, zero reflection
            }
            if (entity.getTimestamp() != null) {
                writer.writeAttribute("timestamp", entity.getTimestamp().toString());
            }
            writer.writeEndElement();
        }
    }
  • Performance Impact: Direct method calls allow HotSpot C2 to completely inline serialization into the call site, operating at near-handwritten speed with zero reflection overhead.

2. GraalVM Native Image AOT Reachability (Zero Reflection Config)

  • In cloud-native microservices (Spring Boot 3 / Quarkus) compiled via GraalVM Native Image (native-image), JAXB requires manually registering hundreds of classes and private members in reflect-config.json.
  • With PolyXML's static builders and optional companion compile-time codecs, the entire serialization graph is 100% statically reachable.
  • Impact: Instant sub-15ms container cold-starts, minimal memory footprint (under 35MB RSS), and zero reflection configuration files.

3. Direct UTF-8 & Compact Strings (Zero Allocation Churn)

  • Traditional Java XML parsing creates millions of short-lived java.lang.String instances during parsing ("name", "id", "CONFIRMED"), putting severe pressure on the Garbage Collector.
  • PolyXML can generate static UTF-8 byte constants (byte[]) for all known tag names, attribute keys, and enumeration literals. Tag matching is performed directly against raw incoming byte streams before ever allocating a String on the JVM heap.

4. Project Panama Direct Memory Streaming (Java 22+ JEP 454)

  • Using Java 22's Foreign Function & Memory API (java.lang.foreign.MemorySegment and Arena), PolyXML can parse structured files directly out of native off-heap memory or memory-mapped files without intermediate ByteBuffer heap copies.

5. Project Loom Virtual Thread Friendliness (Java 21 JEP 444)

  • Complex reactive async programming models (WebFlux, Mutiny, Netty) are notoriously difficult to debug.
  • Because PolyXML streaming serializers and deserializers operate cleanly over standard blocking InputStream and OutputStream, they run seamlessly on lightweight Java 21 Virtual Threads (Executors.newVirtualThreadPerTaskExecutor()). An application can scale to 100,000+ concurrent XML streaming pipelines with plain, straightforward synchronous code.

🔬 Benchmarking & Tradeoff Analysis

To determine the concrete trade-offs between immutable record, mutable pojo, reflection-based Jackson, and direct compile-time codecs, a comprehensive JMH benchmark must be established.

Benchmark Setup & Tooling

  • Framework: Java Microbenchmark Harness (JMH) with -prof gc (allocation profiler) and Java Flight Recorder (JFR).
  • Workloads:
    1. Financial Batch: 10,000 ISO 20022 pacs.008 settlement messages (~25 MB).
    2. Telemetry Stream: 50,000 USAF UCI EntityMt track reports (~35 MB).
  • Competitors / Baselines:
    1. Legacy JAXB (jakarta.xml.bind.JAXBContext + xjc generated classes).
    2. Jackson XML (XmlMapper reading into PolyXML --style pojo).
    3. PolyXML Direct Companion Codec (--codec direct + StAX XMLStreamReader).
    4. PolyXML Zero-Copy Panama FFI (polyxml-c + Java 22 MemorySegment).

Metrics to Measure

  1. Throughput (ops/sec & MB/s): Ingestion rate across single-threaded and virtual thread executors.
  2. Allocation Rate (bytes/op): Heap allocations measured via JMH -prof gc to evaluate GC pause pressure.
  3. Warmup Penalty (Cold Start vs Steady State): Number of iterations required to reach peak C2 JIT throughput (measuring the reflection penalty of JAXB/Jackson).
  4. GraalVM Native Image Compilation Time & Binary Size: Verifying 100% static reachability with zero reflect-config.json.

Hypothesized Tradeoffs & Evaluation Criteria

  • Immutable Record vs Mutable POJO:
    • Tradeoff: Modern JVMs optimize records via scalar replacement (EA - Escape Analysis). However, when performing in-place pipeline mutations (modifying 1 field in an 80-field object), records require a full copy.
    • Hypothesis: For read-only ingestion, record matches or edges out pojo. For mutating message broker pipelines, pojo mutability reduces GC churn by $&gt; 40%$.
  • Jackson Reflection vs Direct Companion Codecs:
    • Hypothesis: --codec direct will outperform Jackson by $\ge 3\times$ in steady-state and $\ge 8\times$ during the first 50 iterations due to zero reflection warmup.

🛠️ Proposed Solution

Introduce a unified --style (or --class-style) CLI flag and configuration option in polyxml.toml:

1. Style Selection: --style <record|pojo>

  • --style record (Default): Current Java 21+ record and C# 12 record output.
  • --style pojo (or --style class): Traditional mutable class generation.

2. Fluent Builder Support: --builder

  • Generate an inner or standalone fluent Builder pattern for complex classes/records:
    var entity = EntityMt.builder()
        .id("UUID-1234")
        .timestamp(Instant.now())
        .build();
    This completely solves the constructor parameter explosion problem even when using immutable records.

3. Codec Mode: --codec <annotation|direct>

  • --codec annotation (Default): Annotates POJOs with Jackson (@JsonProperty, @JacksonXmlProperty) or Jakarta Validation.
  • --codec direct: In addition to POJOs, generates a zero-reflection companion *Codec.java for maximum HotSpot C2 JIT inlining and GraalVM AOT native-image compatibility.

📋 Design Specifications

A. Java Traditional POJO Generation (use_records = false)

When --style pojo is selected:

  • Class Structure:

    public class EntityMt implements Serializable {
        private String id;
        private Instant timestamp;
        private List<SubItem> items = new ArrayList<>();
    
        // 1. No-arg default constructor
        public EntityMt() {}
    
        // 2. Standard JavaBean Getters & Setters
        public String getId() { return this.id; }
        public void setId(String id) { this.id = id; }
    
        public Instant getTimestamp() { return this.timestamp; }
        public void setTimestamp(Instant timestamp) { this.timestamp = timestamp; }
    
        // 3. JAXB-compatible collection accessors
        public List<SubItem> getItems() {
            if (this.items == null) {
                this.items = new ArrayList<>();
            }
            return this.items;
        }
        public void setItems(List<SubItem> items) { this.items = items; }
    
        // 4. Standard Object overrides
        @Override public boolean equals(Object o) { ... }
        @Override public int hashCode() { ... }
        @Override public String toString() { ... }
    }
  • Jackson Integration (--backend jackson):
    Field-level and getter/setter annotations (@JsonProperty, @JacksonXmlProperty, @JacksonXmlElementWrapper) are emitted on properties or getters.

  • Class Inheritance:
    When a complexType extends a base complexType, emit true Java inheritance:

    public class Vehicle extends BaseAsset { ... }

B. C# Traditional Class Generation

When --style class is selected:

  • Emits standard C# classes with parameterless constructors and mutable auto-properties:
    public class EntityMt {
        [JsonPropertyName("id")]
        public string? Id { get; set; }
    
        [JsonPropertyName("timestamp")]
        public DateTimeOffset? Timestamp { get; set; }
    }

💻 CLI & Configuration Usage

CLI Invocations:

# Java: Generate traditional JavaBean POJOs
polyxml generate schema.xsd --lang java --style pojo

# Java: Generate POJOs with Jackson annotations and Builders
polyxml generate schema.xsd --lang java --style pojo --backend jackson --builder

# Java: Generate POJOs with compile-time zero-reflection direct codecs (GraalVM AOT friendly)
polyxml generate schema.xsd --lang java --style pojo --builder --codec direct

# C#: Generate traditional mutable classes
polyxml generate schema.xsd --lang csharp --style class

polyxml.toml Workspace Configuration:

[[generate]]
target = "java"
package = "com.enterprise.uci"
style = "pojo"       # "record" (default) or "pojo"
builder = true        # generate fluent builder
backend = "jackson"   # standard or jackson
codec = "direct"      # "annotation" (default) or "direct"

✅ Acceptance Criteria

  • Add --style <record|pojo>, --builder, and --codec <annotation|direct> flags to GenerateArgs in polyxml-cli.
  • Support style = "record" | "pojo", builder = true | false, and codec = "annotation" | "direct" in polyxml.toml manifest parser.
  • Wire use_records: bool, emit_builder: bool, and emit_direct_codec: bool into JavaOptions in polyxml-core.
  • Implement Java POJO code generation:
    • No-arg default constructor.
    • Private fields with JavaBean get* / set* / is* accessors.
    • Defensive JAXB-style list accessors with auto-initialization.
    • True public class Derived extends Base inheritance.
    • Standard equals, hashCode, and toString.
    • Jackson annotation placement for POJOs when --backend jackson is specified.
  • Implement --builder pattern generator for Java.
  • Implement --codec direct companion serializer/deserializer generator for zero-reflection HotSpot inlining and GraalVM AOT native compilation.
  • Implement mutable auto-property class generation for C# (--style class).
  • Implement JMH benchmark suite comparing JAXB vs Jackson vs Direct Codec vs Panama FFI.
  • Add unit tests in test_java_codegen.rs and test_csharp_codegen.rs validating compile-ready output and Jackson serialization.
  • Update documentation in docs/guides/java.md and docs/guides/csharp.md.

Activity

  1. added
    enhancementNew feature or request
    codegenPolyXML polyglot code generation
    target:javaJava code generator target
    target:csharpC# / .NET code generator target
    cliPolyXML CLI and workspace configuration
    on Sep 22, 2026
  2. nth-bailey commented on Sep 22, 2026

    @nth-bailey
    CollaboratorAuthor

    Measured results: the 3×/8× hypotheses are not supported — please treat them as unconfirmed

    Update from the implementation side (PR #52, branch feat/mutable-model-styles-direct-codecs), so the $\ge 3\times steady-state and $\ge 8\times cold-start hypotheses in this issue are corrected before anyone quotes them.

    Setup: generated POJO + --codec direct models for the benchmark workload schema, JDK 25.0.4.1, Woodstox as the StAX provider for both the direct codec and XmlMapper, 40k messages warmup + 120k messages measured per variant.

    path read (ns/msg) write (ns/msg)
    direct codec, convenience overload (as originally generated) 31,549 31,111
    direct codec, shared/cached factory 4,842 5,885
    Jackson XmlMapper 4,287 7,876

    Conclusions:

    1. Read parity, not a 3×/8× win. With the factory confound removed, the direct codec is roughly at parity with Jackson on reads (~12% behind here) and ~25% faster on writes. The reflection-warmup advantage did not materialize as a throughput advantage on this workload.
    2. Most of the apparent gap was self-inflicted. The original convenience overloads called XMLInputFactory.newFactory()/XMLOutputFactory.newFactory() per message — a ~6.5× cost from StAX provider lookup alone, visible in the JMH smoke as both the lowest ops/s and ~200 KB/msg allocation. PR feat(codegen): add mutable model styles, builders, and direct Java codecs #52 now caches those factories in per-thread fields, and the benchmark harness gained directReadReuse/directWriteReuse variants so future runs measure the codec rather than provider lookup.
    3. What the codecs are actually worth is orthogonal to throughput: zero runtime dependencies (JDK StAX only), no reflection (GraalVM AOT reachability without reflect-config.json), hardened defaults (DTD/external entities disabled), and a serializer for JAXB migrations that does not require adopting Jackson.
    4. POJO vs record mutation: in-place setStatus on the POJO ran ~10–18% faster with ~10% less allocation than the record path's full 80-component reconstruction — directionally consistent with the mutation hypothesis, but well short of the proposed ">40% GC churn" reduction, and at smoke-level confidence.

    Any throughput claim should come from a full JMH run (3 warmup / 5 measure / 2 forks) on the target schema; the smoke configuration is an execution check only.

  3. nth-bailey commented on Sep 22, 2026

    @nth-bailey
    CollaboratorAuthor

    Full JMH run results (replacing the ad-hoc numbers in my earlier comment)

    Config: 3 warmup / 5 measurement iterations, 2 forks, -prof gc, JDK 25.0.4.1, errors are 99.9% CI. Settlement = 10,000 msgs/batch (~2.5 KB each); telemetry = 50,000 msgs/batch (~0.7 KB each). Code includes the per-thread StAX factory caching from PR #52.

    Reads (ops/s per batch; B/msg = Java heap allocation per message)

    settlement B/msg telemetry B/msg
    direct codec (POJO) 22.09 ± 4.48 4,520 7.11 ± 2.04 2,720
    direct codec, reused factory 21.98 ± 5.51 4,520 7.92 ± 0.30 2,720
    Jackson XmlMapper 22.93 ± 0.86 5,040 7.81 ± 0.32 3,240
    record codec 12.08 ± 0.48 4,520 3.10 ± 0.03 2,720
    JAXB (xjc) 10.94 ± 0.61 7,720 3.20 ± 0.23 4,120
    Panama native † 12.42 ± 0.31 7,976 2.79 ± 0.03 7,976

    Writes (ops/s per batch)

    settlement telemetry
    direct codec 15.99 ± 0.42 8.73 ± 0.65
    record codec 16.68 ± 0.47 8.22 ± 0.19
    Panama native 18.04 ± 0.21 4.51 ± 0.09
    Jackson 13.67 ± 1.17 7.32 ± 1.18
    JAXB 8.96 ± 0.54 4.44 ± 0.35

    Mutation (parse → change status → serialize)

    settlement telemetry B/msg
    POJO (in-place setter) 8.80 ± 2.17 3.81 ± 1.05 13,992 / 8,592
    record (full reconstruction) 6.36 ± 0.78 2.06 ± 0.10 14,328 / 8,928

    † Panama read returns a sampled ID rather than a full model; off-heap native allocation is excluded from B/msg.

    Conclusions (supersede my earlier comment's ad-hoc figures):

    1. Read: statistical parity with Jackson. Confidence intervals overlap on both workloads (settlement: Jackson [22.1, 23.8] vs direct [17.6, 26.6]). The hypothesized ≥3× read advantage is not supported — rigorously.
    2. Write: a modest, real win — direct beats Jackson by +17% (settlement, CIs disjoint) and +19% (telemetry, borderline overlap), not 3×.
    3. Allocation: direct codecs allocate 10–16% less heap than Jackson per message on reads; JAXB is worst (7,720 B/msg).
    4. Mutation: POJO in-place setters run 1.38× / 1.85× faster than record reconstruction (telemetry CIs disjoint), but allocation differs by only 2–4% — the proposed ">40% GC churn reduction" is not supported; both paths are parse-dominated.
    5. New observation: record reads are 1.8–2.3× slower than POJO reads on this sparse schema (73 of 80 fields empty) because each empty optional still allocates an Optional component in the canonical constructor. Workload-specific, but it argues for --style pojo on sparse-message ingestion.

    Single host, 2 forks — repeat before quoting. Investigation of the Panama settlement/telemetry inversion (18.04 vs 4.51 ops/s) is deferred.

  4. nth-bailey commented on Sep 23, 2026

    @nth-bailey
    CollaboratorAuthor

    The implementation requested by this issue shipped in PR #52 and is documented in the Java and C# guides. Audited against the acceptance checklist on current main:

    • Java record/pojo styles, C# mutable class style, and Java builder/direct-codec generation work through direct CLI use and both manifest forms. The current spellings are --style, --feature builder, and --feature direct-codec; standalone --builder and --codec flags were removed during the unified-options work in [CLI & Architecture] Consolidate target-specific boolean flags into unified options (--backend, --style, --feature) #50/[CLI] Decide compatibility policy for unified options #58.
    • Generated Java POJOs have no-arg constructors, JavaBean getters/setters, list accessors, inheritance, value methods, and Jackson annotations. Builders and direct StAX codecs are covered by compile-and-run tests. C# mutable classes are covered by XML/JSON round trips, including source-generated JSON. The Java JMH suite compares JAXB, Jackson, direct codecs, records, and Panama with documented workload limits.
    • Current targeted verification passed: Java codegen 15 tests, C# codegen 8 tests, and CLI 34 tests.

    The proposed 3×/8× direct-codec speedups and >40% allocation reduction were hypotheses; the full JMH results in the earlier comment did not support them. GraalVM native-image startup, size, RSS, and configuration were not measured by JMH and are tracked separately in #68. The issue body's optional Jakarta Validation and raw UTF-8 ideas are not claimed as delivered by this implementation; the Java guide describes the supported facet checks and dependencies.

    Closing the implemented Java/C# mutable-model work here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    cliPolyXML CLI and workspace configurationcodegenPolyXML polyglot code generationenhancementNew feature or requesttarget:csharpC# / .NET code generator targettarget:javaJava code generator target

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions