Skip to content

Java

PolyXML supports Java 22 or newer for both generated Java applications and the published Java binding. Java 25 is a good LTS choice. Generated models and direct StAX codecs use only Java APIs; they do not automatically call the Rust runtime. Use the Panama binding below when your application needs native PolyXML parsing.

Java 22 models: records, POJOs, and builders

Records remain the default. Select mutable JavaBeans for setter-based frameworks:

polyxml generate schema.xsd --lang java --style pojo --feature builder --out generated
polyxml generate schema.xsd --lang java --style pojo --backend jackson --feature builder --out generated
polyxml generate schema.xsd --lang java --style record --feature builder --out generated

--style class is an alias for pojo. Mutable complex types have a public no-argument constructor, private fields, getX/setX accessors (isX for primitive booleans), Serializable, and value-based equals, hashCode, and toString. XSD extensions use Java inheritance. Optional scalar properties are nullable boxed values; repeated properties are mutable lists. A list getter initializes the list if it was set to null, so order.getItems().add(item) works. Simple-value wrappers and choice branches also use mutable classes in this mode.

var entity = EntityMt.builder().id("UUID-1234").build();
entity.setStatus("PROCESSED");

Builders include inherited fields and produce a fresh object on each build(). POJO builders copy their list containers; nested objects remain shared. Record builders use the canonical constructor and its facet checks. When builders or direct codecs are enabled, records flatten inherited fields into their components. POJO setters enforce supported facets, but a no-argument constructor permits a partially populated object.

On schemas where most fields are optional and empty, POJO construction is cheaper than record construction: every empty optional still allocates an Optional component in the record's canonical constructor. The JMH suite measured POJO reads at roughly 1.8–2.3× record reads on its sparse 80-field message (73 fields empty); dense messages narrow the gap. Choose the representation for its ergonomics and benchmark your own schema before optimizing for this. This is not full XSD validation. The Jackson backend annotates fields explicitly and disables automatic bean-property discovery to avoid duplicate properties after XML names are converted to Java identifiers. It requires Jackson 2.x XML/annotations; standard POJOs and direct codecs require only the JDK. No Jakarta Validation dependency is introduced.

Direct streaming XML codecs

polyxml generate schema.xsd --lang java --style pojo --feature builder,direct-codec --out generated

Annotation-based codecs are the default and emit only models. --feature direct-codec adds a companion TypeNameCodec.java for every generated type, with statically linked getters/setters, constructors, and nested codecs. It works with either record or POJO models and can be combined with Jackson annotations. It uses StAX from the JDK, without reflection or a native library:

try (var input = java.nio.file.Files.newInputStream(path)) {
    EntityMt entity = EntityMtCodec.readXml(input);
    entity.setStatus("PROCESSED");
    try (var output = java.nio.file.Files.newOutputStream(destination)) {
        EntityMtCodec.writeXml(entity, output);
    }
}

The stream overloads create and close their StAX reader/writer and leave ownership of the supplied stream with the caller. Their XMLInputFactory/XMLOutputFactory are cached in a per-thread field, so repeated calls do not repeat StAX provider lookup or security-property setup. For reuse inside a larger stream, pass an XMLStreamReader positioned at a start element or an XMLStreamWriter. Reading leaves the cursor at the matching end element. The writer overload accepting local and ns selects a particular root element when a type has multiple XML roots. Otherwise, the first matching root declaration is used, or the type name if no root is declared.

Supported bindings include attributes, text, nested and recursive models, enums, simple restrictions, nullable values, repeated elements, XML lexical lists in the IR, choice wrappers, namespaces, XML booleans, and binary values. Unknown elements are skipped as complete subtrees. Stream-based readers disable DTDs and external entities; callers supplying a StAX reader configure their own parser. The codecs follow the normalized IR: choice wrappers retain their generated wrapper representation, and this is not a general XSD validator or a replacement for missing schema-parser features. Wildcard fields and xs:anyType are rejected by the CLI in direct mode. Runtime xsi:type dispatch is rejected; use the concrete type's codec and XML without runtime type overrides. Passing a derived POJO to a base codec is also rejected instead of silently dropping derived fields. Rust API callers should call validate_direct_codecs(&ir) before generation. Models containing cycles are representable, but serializing an actual cyclic object graph is not supported.

[[generate]]
target = "java"
output = "generated/java"
package = "com.enterprise.models"
style = "pojo"
features = ["builder", "direct-codec"]
backend = "jackson"

The same options work under [codegen.java]. See the Java benchmark harness for JAXB, Jackson, direct POJO/record, and Panama comparisons. No throughput advantage is assumed; benchmark the relevant schema and workload.

Java 22+ native bindings

The Panama binding requires a native PolyXML library in addition to Java 22+.

PolyXML provides native C/Rust XML data-binding for the modern Java Virtual Machine using Java 22+ Project Panama (Foreign Function & Memory API - JEP 454). It completely eliminates legacy JNI glue code, GC object pinning, and JNI transition overheads by leveraging native off-heap memory and downcall method handles.


📦 Build Configuration

Maven (pom.xml)

<dependencies>
    <dependency>
        <groupId>io.github.polyxml</groupId>
        <artifactId>polyxml</artifactId>
        <version>0.23.2</version>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <release>22</release>
            </configuration>
        </plugin>
    </plugins>
</build>

JVM Runtime Flags

Because Project Panama accesses native off-heap memory, your application requires the --enable-native-access JVM flag at runtime:

java --enable-native-access=ALL-UNNAMED -jar target/my-app.jar

1. Schema Construction & Resource Management

PolyXML schemas are native off-heap objects. PolyXML.Schema implements AutoCloseable, enabling clean deterministic cleanup with Java's standard try-with-resources:

package com.example;

import io.polyxml.PolyXML;

public class Application {
    public static void main(String[] args) {
        System.out.println("PolyXML Native Core Version: " + PolyXML.version());

        // Build an off-heap native schema using try-with-resources
        try (PolyXML.Schema schema = new PolyXML.SchemaBuilder("ServerMetrics")
                .addField("serverId", "id", PolyXML.FieldKind.ATTRIBUTE, PolyXML.ScalarType.INT)
                .addField("hostname", "host", PolyXML.FieldKind.ELEMENT, PolyXML.ScalarType.STRING)
                .addField("cpuUtilization", "cpu", PolyXML.FieldKind.ELEMENT, PolyXML.ScalarType.FLOAT)
                .addField("isHealthy", "healthy", PolyXML.FieldKind.ELEMENT, PolyXML.ScalarType.BOOL)
                .build()) {

            System.out.println("Schema created successfully in off-heap memory.");
        }
    }
}

2. End-to-End Deserialization & Serialization

Deserialize XML strings or byte buffers into native PolyXML.Value objects and serialize back to XML:

package com.example;

import io.polyxml.PolyXML;

public class SerializationExample {
    public static void main(String[] args) {
        try (PolyXML.Schema schema = new PolyXML.SchemaBuilder("Sensor")
                .addField("id", "id", PolyXML.FieldKind.ATTRIBUTE, PolyXML.ScalarType.INT)
                .addField("name", "name", PolyXML.FieldKind.ELEMENT, PolyXML.ScalarType.STRING)
                .addField("reading", "reading", PolyXML.FieldKind.ELEMENT, PolyXML.ScalarType.FLOAT)
                .addField("calibrated", "calibrated", PolyXML.FieldKind.ELEMENT, PolyXML.ScalarType.BOOL)
                .build()) {

            String xml = "<Sensor id=\"101\"><name>Barometer</name><reading>1013.25</reading><calibrated>true</calibrated></Sensor>";

            // 1. Deserialize off-heap
            try (PolyXML.Value val = PolyXML.deserialize(xml, schema)) {
                long id = val.getField("id").getInt().orElse(0L);
                String name = val.getField("name").getString().orElse("");
                double reading = val.getField("reading").getFloat().orElse(0.0);
                boolean calibrated = val.getField("calibrated").getBool().orElse(false);

                System.out.printf("Sensor %d [%s]: %.2f (Calibrated: %b)%n", id, name, reading, calibrated);

                // 2. Serialize back to XML string with 2-space indentation
                String outputXml = PolyXML.serializeToString("Sensor", val, schema, 2);
                System.out.println("Output XML:\n" + outputXml);
            }
        }
    }
}

3. XML Namespaces & Prefix Mapping

Declare model and field namespaces, and serialize with custom prefix mappings:

package com.example;

import io.polyxml.PolyXML;
import java.util.Map;

public class NamespaceExample {
    public static void main(String[] args) {
        try (PolyXML.Schema schema = new PolyXML.SchemaBuilder("Order")
                .setNamespace("https://example.com/orders")
                .addField("id", "id", PolyXML.FieldKind.ATTRIBUTE, PolyXML.ScalarType.INT)
                .addField("item", "item", PolyXML.FieldKind.ELEMENT, PolyXML.ScalarType.STRING, "https://example.com/items")
                .build()) {

            String xml = "<ns0:Order xmlns:ns0=\"https://example.com/orders\" xmlns:ns1=\"https://example.com/items\" id=\"888\"><ns1:item>JavaGadget</ns1:item></ns0:Order>";

            try (PolyXML.Value val = PolyXML.deserialize(xml, schema)) {
                long id = val.getField("id").getInt().orElse(0L);
                String item = val.getField("item").getString().orElse("");
                System.out.printf("Order #%d: %s%n", id, item);

                // Serialize with custom prefix mapping
                Map<String, String> nsMap = Map.of(
                    "ord", "https://example.com/orders",
                    "itm", "https://example.com/items"
                );

                byte[] bytes = PolyXML.serializeWithOptions("Order", val, schema, 2, true, nsMap);
                System.out.println(new String(bytes, java.nio.charset.StandardCharsets.UTF_8));
            }
        }
    }
}

4. Off-Heap Confined Arenas & Zero-GC Pressure

When passing large XML strings or byte streams from Java into PolyXML, Java 22's Arena.ofConfined() allocates off-heap memory segments that bypass the JVM garbage collector entirely:

import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.nio.charset.StandardCharsets;

public class OffHeapBufferExample {
    public static void allocateAndPassXml(String xmlData) {
        // Arena confines allocation to current thread and frees immediately upon exit
        try (Arena arena = Arena.ofConfined()) {
            byte[] xmlBytes = xmlData.getBytes(StandardCharsets.UTF_8);
            MemorySegment nativeBuffer = arena.allocate(xmlBytes.length);
            nativeBuffer.copyFrom(MemorySegment.ofArray(xmlBytes));

            System.out.printf("Allocated %d bytes off-heap with zero GC pressure%n", nativeBuffer.byteSize());
            // Pass nativeBuffer.address() to PolyXML native routines...
        } // Instant off-heap deallocation occurs here
    }
}

5. Enterprise Architecture: ISO 20022 Batch Processing

In high-throughput enterprise architectures (e.g. processing millions of ISO 20022 XML financial payment messages or HL7 clinical records):

  1. Singleton Native Schemas: Store PolyXML.Schema instances in static final fields or Spring Singleton beans so they are created once at application startup.
  2. Eliminate Garbage Collection Pauses: By streaming raw socket or file bytes into off-heap MemorySegment buffers, you prevent millions of short-lived XML DOM strings from exhausting the JVM Young Generation heap.
  3. Thread Safety: PolyXML's native schema handles are immutable and read-only after construction, making them safe to share concurrently across all JVM virtual threads (Project Loom).

6. Enterprise Jackson Backend (--backend jackson)

PolyXML's code generator supports an opt-in Jackson backend that annotates generated Java 22+ records with Jackson annotations for seamless integration with Spring Boot 3, Quarkus, Micronaut, and any framework using ObjectMapper or XmlMapper.

Quick Start

CLI:

polyxml generate \
  --lang java \
  --backend jackson \
  --package com.enterprise.banking \
  --out src/main/java/com/enterprise/banking \
  schemas/pacs_008_core.xsd

polyxml.toml:

[[generate]]
target = "java"
output = "src/main/java/com/enterprise/banking"
package = "com.enterprise.banking"
backend = "jackson"

What Gets Generated

When --backend jackson is active, PolyXML emits the following annotations:

Annotation Applied To Purpose
@JsonIgnoreProperties(ignoreUnknown = true) Record class Forward-compatible deserialization
@JsonInclude(NON_EMPTY) Record class + optional/list fields Skip empty values during serialization
@JacksonXmlRootElement(localName, namespace) Record class XML root element binding
@JsonProperty("...") Record components JSON field name mapping
@JacksonXmlProperty(localName, isAttribute, namespace) Record components XML attribute vs. element discrimination
@JacksonXmlElementWrapper(useWrapping = false) List components Unboxed XML sequences
@JsonValue / @JsonCreator Enum getValue() / fromValue() Enum string serialization
@JsonTypeInfo / @JsonSubTypes / @JsonTypeName Sealed interfaces (choice types) Polymorphic type discrimination
@JsonValue / @JacksonXmlText / @JsonCreator Simple type wrappers Transparent value serialization

Maven Dependencies

Add Jackson XML to your project:

<dependencies>
    <dependency>
        <groupId>com.fasterxml.jackson.dataformat</groupId>
        <artifactId>jackson-dataformat-xml</artifactId>
        <version>2.18.3</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.datatype</groupId>
        <artifactId>jackson-datatype-jdk8</artifactId>
        <version>2.18.3</version>
    </dependency>
</dependencies>

Spring Boot 3 Usage Example

package com.enterprise.banking;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

public class PaymentProcessor {
    private static final XmlMapper XML = new XmlMapper();
    private static final ObjectMapper JSON = new ObjectMapper();

    public CreditTransfer parseXml(String xml) throws Exception {
        return XML.readValue(xml, CreditTransfer.class);
    }

    public String toJson(CreditTransfer transfer) throws Exception {
        return JSON.writeValueAsString(transfer);
    }
}

Note: The default --backend standard (or no --backend) continues to emit pure, zero-dependency Java 22+ records with no Jackson imports.