Repository navigation
[Codegen: Java & C#] Support traditional mutable classes/POJOs with getters, setters, and builders (--style pojo / --builder) #46
Description
🎯 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:
-
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
recordtypes only provide canonical constructors and component accessor methods namedid()instead ofgetId(), which breaks older reflection-based bean introspection and dynamic proxying.
- Enterprise Java frameworks (Hibernate / JPA, MyBatis, Spring Data, ModelMapper, Dozer, MapStruct, Apache Commons BeanUtils) require a parameterless default constructor (
-
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());
- Complex enterprise schemas—such as USAF UCI (
-
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.
- In telematics gateways, message routers, and financial settlement switches, an application often receives an XML message, updates 1 or 2 fields (e.g.
-
Drop-in JAXB (
xjc) &xsd.exeMigration:- Enterprises with 10- to 20-year-old codebases written against
xjc-generated classes often have thousands of lines of code expectingorder.getItems().add(...)orentity.setStatus(...). Providing traditional classes enables zero-refactoring, drop-in replacement of JAXB.
- Enterprises with 10- to 20-year-old codebases written against
🚀 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:
-
Modern Date/Time Types (
java.time.*) vs.XMLGregorianCalendar:- JAXB generates archaic
XMLGregorianCalendarandjavax.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, andLocalTime.
- JAXB generates archaic
-
Elimination of
JAXBElement<T>&ObjectFactoryHell:- 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-lineObjectFactory.javaboilerplate files. - PolyXML generates clean, idiomatic Java types with direct nullable fields or standard
Optional<T>accessors—zeroJAXBElement<T>wrappers and zeroObjectFactoryclutter.
- For nillable, substitution, or optional choice elements, JAXB wraps fields in cumbersome
-
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 (
@JsonPropertyand@JacksonXmlProperty), allowing the exact same POJO to serialize and deserialize to both XML and JSON seamlessly.
-
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@Validcontroller and service boundaries.
- JAXB completely discards XSD facets (
-
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()).
- 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 (
-
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.
- JAXB depends on heavy runtime reflection (
-
Zero Jakarta/JAXB Dependency Clashes:
- JAXB was removed from the JDK in Java 11, creating constant classpath conflicts (
javax.xml.bindvsjakarta.xml.bindvsjaxb-runtime). PolyXML POJOs run cleanly with standard Java libraries or standard Jackson already present in modern Spring/Quarkus/Micronaut apps.
- JAXB was removed from the JDK in Java 11, creating constant classpath conflicts (
⚡ 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 inreflect-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.Stringinstances 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 aStringon the JVM heap.
4. Project Panama Direct Memory Streaming (Java 22+ JEP 454)
- Using Java 22's Foreign Function & Memory API (
java.lang.foreign.MemorySegmentandArena), PolyXML can parse structured files directly out of native off-heap memory or memory-mapped files without intermediateByteBufferheap 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
InputStreamandOutputStream, 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:
- Financial Batch: 10,000 ISO 20022
pacs.008settlement messages (~25 MB). - Telemetry Stream: 50,000 USAF UCI
EntityMttrack reports (~35 MB).
- Financial Batch: 10,000 ISO 20022
- Competitors / Baselines:
- Legacy JAXB (
jakarta.xml.bind.JAXBContext+xjcgenerated classes). - Jackson XML (
XmlMapperreading into PolyXML--style pojo). - PolyXML Direct Companion Codec (
--codec direct+ StAXXMLStreamReader). - PolyXML Zero-Copy Panama FFI (
polyxml-c+ Java 22MemorySegment).
- Legacy JAXB (
Metrics to Measure
- Throughput (ops/sec & MB/s): Ingestion rate across single-threaded and virtual thread executors.
- Allocation Rate (bytes/op): Heap allocations measured via JMH
-prof gcto evaluate GC pause pressure. - Warmup Penalty (Cold Start vs Steady State): Number of iterations required to reach peak C2 JIT throughput (measuring the reflection penalty of JAXB/Jackson).
- 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,
recordmatches or edges outpojo. For mutating message broker pipelines,pojomutability reduces GC churn by$> 40%$ .
-
Jackson Reflection vs Direct Companion Codecs:
-
Hypothesis:
--codec directwill outperform Jackson by$\ge 3\times$ in steady-state and$\ge 8\times$ during the first 50 iterations due to zero reflection warmup.
-
Hypothesis:
🛠️ 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+recordand C# 12recordoutput.--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:
This completely solves the constructor parameter explosion problem even when using immutable records.
var entity = EntityMt.builder() .id("UUID-1234") .timestamp(Instant.now()) .build();
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.javafor 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 acomplexTypeextends a basecomplexType, 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 classpolyxml.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 toGenerateArgsinpolyxml-cli. - Support
style = "record" | "pojo",builder = true | false, andcodec = "annotation" | "direct"inpolyxml.tomlmanifest parser. - Wire
use_records: bool,emit_builder: bool, andemit_direct_codec: boolintoJavaOptionsinpolyxml-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 Baseinheritance. - Standard
equals,hashCode, andtoString. - Jackson annotation placement for POJOs when
--backend jacksonis specified.
- Implement
--builderpattern generator for Java. - Implement
--codec directcompanion 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.rsandtest_csharp_codegen.rsvalidating compile-ready output and Jackson serialization. - Update documentation in
docs/guides/java.mdanddocs/guides/csharp.md.
Activity
nth-bailey commentedon Sep 22, 2026
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\timessteady-state and$\ge 8\timescold-start hypotheses in this issue are corrected before anyone quotes them.Setup: generated POJO +
--codec directmodels for the benchmark workload schema, JDK 25.0.4.1, Woodstox as the StAX provider for both the direct codec andXmlMapper, 40k messages warmup + 120k messages measured per variant.XmlMapperConclusions:
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 gaineddirectReadReuse/directWriteReusevariants so future runs measure the codec rather than provider lookup.reflect-config.json), hardened defaults (DTD/external entities disabled), and a serializer for JAXB migrations that does not require adopting Jackson.setStatuson 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.
fix(java): cache direct-codec StAX factories per thread
nth-bailey commentedon Sep 22, 2026
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)
XmlMapperxjc)Writes (ops/s per batch)
Mutation (parse → change status → serialize)
† 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):
Optionalcomponent in the canonical constructor. Workload-specific, but it argues for--style pojoon 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.
fix(java): cache direct-codec StAX factories per thread
chore(release): 0.20.0 [skip ci]
nth-bailey commentedon Sep 23, 2026
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:record/pojostyles, C# mutableclassstyle, and Javabuilder/direct-codecgeneration work through direct CLI use and both manifest forms. The current spellings are--style,--feature builder, and--feature direct-codec; standalone--builderand--codecflags 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.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.