Skip to content

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

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

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