-
Notifications
You must be signed in to change notification settings - Fork 9
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.
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.
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());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.
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.
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.
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.