Skip to content

Mixed-Format Projects: XML Envelope with an Embedded JSON Payload

A common integration shape: an XML document (XSD-described) where one element's text content is a JSON document (JSON Schema-described). schema2class handles both halves in one Gradle invocation — no IR-level linkage needed, and none is performed: the JSON-bearing element stays a String in the envelope class, and the payload class is generated from its own schema.

Build configuration

plugins {
    kotlin("jvm")
    id("ca.esmcelroy.schema2class")
}

schema2class {
    schemas {
        create("envelope") {
            source = file("schemas/envelope.xsd")
            annotationMode = "XMLUTIL"          // XML round-trip via pdvrieze/xmlutil
            // packageName omitted → derived from targetNamespace
            // xs:import / xs:include resolved transitively
        }
        create("payload") {
            source = file("schemas/payload.schema.json")
            packageName = "com.corp.payload"     // required for JSON Schema
            annotationMode = "KOTLINX_SERIALIZATION"
        }
    }
}

schema2classGenerate runs before compileKotlin automatically (the generated directory is wired into the main source set when the Kotlin JVM plugin is present). For Jackson output, set omitNulls = true on a schema to emit @JsonInclude(JsonInclude.Include.NON_NULL). For kotlinx output, keep nullable defaults and configure the runtime encoder with Json { encodeDefaults = false }. For integer-valued enums in Jackson mode, set enumUnknownFallback = true to emit numeric @JsonValue serialization plus a @JsonCreator factory that maps unrecognized wire values to UNKNOWN.

Using the generated classes together

Suppose the envelope XSD declares <xs:element name="payload" type="xs:string"/> and the JSON Schema describes TelemetryPayload:

val payload = TelemetryPayload(deviceId = "dev-42", recordedAt = now, readings = readings)

// Embed: serialize the payload to JSON text, place it in the envelope field
val envelope = Envelope(
    messageId = "m-1",
    payload = Json.encodeToString(payload),
)

// Send: the envelope round-trips as XML
val xmlText = XML.encodeToString(envelope)

// Receive: decode the envelope, then decode the embedded JSON
val received = XML.decodeFromString<Envelope>(xmlText)
val receivedPayload = Json.decodeFromString<TelemetryPayload>(received.payload)

Per-schema configuration reference

Property XSD JSON Schema
source required required
packageName optional — omitted: derive from namespace(s) + resolve imports; set: single-document parse into that package required
packageOverrides namespace URI → package map for multi-file resolution ignored
wireNamespace optional XML wire namespace emitted in xmlutil annotations ignored
wireNamespaceOverrides schema namespace URI → XML wire namespace map for multi-file XSD resolution ignored
nameBindings ignored today optional sidecar file for friendly generated names; see docs/naming-bindings.md
annotationMode NONE / KOTLINX_SERIALIZATION / XMLUTIL / JACKSON same (XMLUTIL adds no value for JSON)
omitNulls in JACKSON mode emits class-level @JsonInclude(JsonInclude.Include.NON_NULL) same; in kotlinx modes, pair nullable defaults with Json { encodeDefaults = false }
enforceConstraints emits require guards for supported ranges, lengths, patterns, and list cardinality same
enumUnknownFallback in JACKSON mode, integer enum restrictions emit numeric wire values and opt-in UNKNOWN fallback same for integer-valued JSON Schema enums; kotlinx numeric unknown fallback is not generated yet

The equivalent CLI flag is --enum-unknown-fallback, used with --annotation-mode jackson.