Skip to content

XML and JSON Binding

Jin Kwon edited this page Aug 29, 2026 · 2 revisions

XML and JSON Binding

Bound metadata types are annotated for Jakarta XML Binding and Jakarta JSON Binding, so a fetched result set can be written to a file, sent over a wire, or diffed between two database products without writing any mapping code.

Both APIs are declared provided and optional, so neither is on your classpath transitively. Add the API and a provider for whichever you use:

Binding API Provider used by this project's tests
XML jakarta.xml.bind:jakarta.xml.bind-api org.glassfish.jaxb:jaxb-runtime
JSON jakarta.json.bind:jakarta.json.bind-api org.eclipse:yasson

Both bindings are field-based, and the package configures that itself, so a default provider instance works with no extra setup on your side. It matters because binding types have private fields and package-private setters: a provider restricted to public members could read them but could not write them back.

Unknown columns are excluded from both bindings, exactly as they are excluded from the Java serialized form. See Model Notes for why.

Jakarta XML Binding

XML output is namespace-qualified. The namespace is published as JakartaXmlBindingConstants.NAMESPACE_URI = https://github.com/jinahya/database-metadata-bind.

Each bound type carries its own @XmlRootElement name — table, column, primaryKey, and so on; see the table in API Reference.

Bootstrapping The Context

The package ships a jaxb.index resource listing every bound class, so a JAXBContext can be created by package name rather than by enumerating classes:

var jaxbContext = JAXBContext.newInstance(MetadataType.class.getPackageName());

Marshalling A List

JAXB cannot marshal a bare java.util.List — a collection must be held by a type annotated with @XmlRootElement. MetadataTypeWrapper<T> is that holder. One generic class serves every element type: the list is marshalled with @XmlAnyElement(lax = true), so each element is written under its own root element name.

var wrapper = new MetadataTypeWrapper<Schema>();
wrapper.getElements().addAll(context.getSchemas());

var marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(wrapper, path.toFile());
<metadataTypes>
  <schema>...</schema>
  <schema>...</schema>
</metadataTypes>

MetadataTypeWrapper exposes a public no-argument constructor, a modifiable getElements() list (created lazily, so it is never null), and the constant ROOT_ELEMENT_NAME = metadataTypes.

The trade-off of a single generic wrapper is that every document shares the one root element metadataTypes rather than a per-type name such as <schemas> or <tables>. The element types must be known to the JAXBContext, which is what bootstrapping by package name (and therefore jaxb.index) takes care of.

Unmarshalling

var object = jaxbContext.createUnmarshaller().unmarshal(path.toFile());
if (object instanceof MetadataTypeWrapper<?> wrapper) {
    List<?> elements = wrapper.getElements();
}

The type variable T is erased at runtime, so the unmarshalled wrapper is raw — cast the elements to the type you wrote.

Jakarta JSON Binding

JSON binding needs no wrapper: JSON-B serializes a List directly to a JSON array.

try (var jsonb = JsonbBuilder.create()) {
    String json = jsonb.toJson(context.getSchemas());
    Schema schema = jsonb.fromJson("{\"tableSchem\":\"PUBLIC\"}", Schema.class);
}

A default JsonbBuilder.create() instance handles binding types correctly because the package declares @JsonbVisibility(JakartaJsonBindingUtils.FieldAccessVisibilityStrategy.class). That strategy makes all fields visible and all methods invisible, so JSON-B binds directly to fields.

It is worth being precise about what that buys, because the arrangement invites a wrong guess. The strategy is required for deserialization only, and only because the setters are not public:

Direction Without the strategy Why
toJson works JSON-B reads through the public getters
fromJson returns an instance with every property null no visible setter to write through

So serialization needs no help at all. Neither does instantiation: PropertyVisibilityStrategy governs fields and methods only, and a provider reaches the protected no-argument constructor on its own.

The causation runs one way and is easy to read backwards. The accessor shape — public getters, non-public setters, chosen so bound rows stay read-only from outside the package — is what obliges this configuration, not the reverse.

Note also that the failure mode is silent: were the annotation removed, nothing would fail to compile and serialization would still work; you would simply get well-formed objects with nothing in them.

JakartaJsonBindingUtils.FieldAccessVisibilityStrategy is public only so a JSON-B provider can instantiate it reflectively from the package annotation. Application code does not need to reference it.

Where The Project Uses This

Both bindings are exercised by the test suite rather than by any public helper on Context. The in-memory database tests and the external integration tests run the same metadata walkthrough and write each collection to target/{product}-{product-version}-{driver}-{driver-version}-{name}.xml and .json, which makes the artifacts easy to diff across database products and driver versions. See Testing and Build and External Integration Tests.

Clone this wiki locally