Fork me on GitHub

Generating Java Classes

This guide explains how to generate Java binding classes from Metaschema modules.

When working with structured data, you have several options: parse XML/JSON manually, use generic tree structures like DOM or JsonNode, or use strongly-typed classes. Strongly-typed classes offer significant advantages:

  • Compile-time safety - The compiler catches type errors before runtime
  • IDE support - Code completion, refactoring, and navigation work automatically
  • Self-documenting code - Method names like getCatalog().getMetadata().getTitle() clearly express intent
  • No boilerplate - Serialization and deserialization are handled automatically

The challenge is keeping your Java classes synchronized with your data model. If the model changes, you need to update the classes—a tedious and error-prone process. The Metaschema Maven plugin solves this by generating Java classes directly from Metaschema module definitions. When your model changes, regenerate the classes and the compiler tells you what code needs updating.

The Metaschema Maven plugin integrates into your Maven build to generate Java classes from Metaschema module definitions. These generated classes provide:

  • Type-safe data binding - Fields have proper Java types matching Metaschema definitions
  • Automatic serialization - Read and write XML, JSON, and YAML without manual parsing
  • IDE integration - Full code completion and type checking in your IDE
  • Compile-time validation - Catch errors at build time rather than runtime

Add to your pom.xml:

<build>
    <plugins>
        <plugin>
            <groupId>dev.metaschema.java</groupId>
            <artifactId>metaschema-maven-plugin</artifactId>
            <version>3.0.0.M2</version>
            <executions>
                <execution>
                    <id>generate-sources</id>
                    <goals>
                        <goal>generate-sources</goal>
                    </goals>
                    <configuration>
                        <metaschemaDir>src/main/metaschema</metaschemaDir>
                        <includes>
                            <include>my-model_metaschema.xml</include>
                        </includes>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
<configuration>
    <!-- Source directory containing Metaschema files -->
    <metaschemaDir>src/main/metaschema</metaschemaDir>

    <!-- Which files to include -->
    <includes>
        <include>**/*_metaschema.xml</include>
    </includes>

    <!-- Files to exclude -->
    <excludes>
        <exclude>**/draft-*.xml</exclude>
    </excludes>

    <!-- Output directory for generated sources -->
    <outputDirectory>/home/runner/work/metaschema-java/metaschema-java/target/generated-sources/metaschema</outputDirectory>
</configuration>
# Generate sources
mvn generate-sources

# Or as part of full build
mvn compile

Generated classes appear in target/generated-sources/metaschema/.

<define-assembly name="catalog">
    <formal-name>Catalog</formal-name>
    <description>A collection of controls.</description>
    <root-name>catalog</root-name>
    <define-flag name="uuid" required="yes">
        <formal-name>Catalog UUID</formal-name>
    </define-flag>
    <model>
        <assembly ref="metadata" min-occurs="1"/>
        <assembly ref="group" max-occurs="unbounded"/>
    </model>
</define-assembly>
@MetaschemaAssembly(
    name = "catalog",
    rootName = "catalog"
)
public class Catalog {
    @BoundFlag(name = "uuid", required = true)
    private UUID uuid;

    @BoundAssembly(minOccurs = 1)
    private Metadata metadata;

    @BoundAssembly(maxOccurs = -1)
    private List<Group> groups;

    // Getters and setters
    public UUID getUuid() { return uuid; }
    public void setUuid(UUID uuid) { this.uuid = uuid; }

    public Metadata getMetadata() { return metadata; }
    public void setMetadata(Metadata metadata) { this.metadata = metadata; }

    public List<Group> getGroups() { return groups; }
    public void setGroups(List<Group> groups) { this.groups = groups; }
}
Metaschema Type Java Type
uuid java.util.UUID
date-time java.time.ZonedDateTime
string String
integer java.math.BigInteger
boolean Boolean
uri java.net.URI
markup-line MarkupLine
markup-multiline MarkupMultiline
Metaschema Java
min-occurs="0" Nullable field
min-occurs="1" @NonNull on getter/setter methods
max-occurs="1" Single instance
max-occurs="unbounded" List<T>

Generated classes include annotations for:

@MetaschemaAssembly    // Assembly definitions
@MetaschemaField       // Field definitions
@BoundFlag             // Flag instances
@BoundField            // Field instances
@BoundAssembly         // Assembly instances
Catalog catalog = new Catalog();
catalog.setUuid(UUID.randomUUID());

Metadata metadata = new Metadata();
metadata.setTitle(MarkupLine.fromMarkdown("My Catalog"));
metadata.setLastModified(ZonedDateTime.now());
catalog.setMetadata(metadata);
import dev.metaschema.databind.IBindingContext;
import dev.metaschema.databind.io.Format;
import dev.metaschema.databind.io.ISerializer;

IBindingContext context = IBindingContext.instance();
ISerializer<Catalog> serializer = context.newSerializer(Format.JSON, Catalog.class);
serializer.serialize(catalog, Path.of("catalog.json"));
IDeserializer<Catalog> deserializer = context.newDeserializer(
    Format.JSON, Catalog.class);
Catalog catalog = deserializer.deserialize(Path.of("catalog.json"));
<configuration>
    <includes>
        <include>core_metaschema.xml</include>
        <include>extension_metaschema.xml</include>
    </includes>
</configuration>

If modules import each other:

<!-- core_metaschema.xml -->
<metaschema>
    <define-assembly name="base">...</define-assembly>
</metaschema>

<!-- extension_metaschema.xml -->
<metaschema>
    <import href="core_metaschema.xml"/>
    <define-assembly name="extended">
        <model>
            <assembly ref="base"/>
        </model>
    </define-assembly>
</metaschema>

All imported modules are processed automatically.

Control package via namespace:

<metaschema xmlns="http://csrc.nist.gov/ns/oscal/metaschema/1.0"
            xmlns:java="http://metaschema.dev/ns/java">
    <schema-name>My Model</schema-name>
    <namespace>http://example.com/ns/mymodel</namespace>
    <!-- Package derived from namespace -->
</metaschema>

The plugin generates abstract base classes for extension:

// Generated abstract class
public abstract class AbstractCatalog {
    // Generated fields and methods
}

// Your extension (in src/main/java)
public class Catalog extends AbstractCatalog {
    // Additional methods
}

Symptom: No classes in target/generated-sources/

Fix:

  1. Check metaschemaDir path is correct
  2. Verify includes pattern matches files
  3. Run mvn generate-sources -X for debug output

Symptom: Generated classes won't compile

Fix:

  1. Ensure all imported modules are accessible
  2. Check for circular dependencies
  3. Verify Metaschema syntax is valid

Symptom: Annotations not found

Fix: Add runtime dependency:

<dependency>
    <groupId>dev.metaschema.java</groupId>
    <artifactId>metaschema-databind</artifactId>
    <version>3.0.0.M2</version>
</dependency>
  1. Use consistent naming - Follow *_metaschema.xml convention
  2. Organize by module - One module per domain concept
  3. Document in Metaschema - Descriptions become Javadoc
  4. Version your schemas - Track schema changes in VCS
  5. Don't edit generated code - Use extension classes instead

Continue learning about the Metaschema Java Tools with these related guides: