The Kotlin ecosystem has a significant gap: no maintained tool generates idiomatic Kotlin from XSD, and no tool of any kind covers both XSD and JSON Schema through one pipeline. A domain survey (see docs/domain-survey.md, July 2026) confirmed the XSD side is an empty niche and identified one active competitor on the JSON Schema side:
| Tool | Language | Schema support | Kotlin output | Programmatic API | Maintained |
|---|---|---|---|---|---|
| JAXB / xjc | Java | XSD | No (Java) | Partial | Yes (Java-only forever) |
| KAXB | Kotlin | XSD | Yes | No | Abandoned (no releases) |
| schema-gen | Groovy | XSD | Partial | No | Dormant, untested |
| jsonschema2pojo | Java | JSON Schema | No (Java) | No (CLI/Maven/Gradle) | Yes |
| json-kotlin-schema-codegen | Kotlin | JSON Schema only | Yes | Yes | Yes (single maintainer) |
| quicktype | TypeScript | JSON Schema | Secondary target | No (Node CLI) | Yes |
| Fabrikt | Kotlin | OpenAPI 3 only | Yes | Yes | Yes |
| kotlinx.serialization | Kotlin | Runtime only | N/A (serialization, not gen) | N/A | Yes |
Developers who need to consume XSD contracts in Kotlin are forced to wrap xjc-generated Java (platform types, no data classes, no sealed hierarchies) or hand-write classes. On the JSON Schema side, json-kotlin-schema-codegen is credible but single-format; teams with mixed XML + JSON contracts need two disjoint toolchains with inconsistent output. Fabrikt proves the architecture (Kotlin-native, KotlinPoet, Gradle plugin) earns adoption — for OpenAPI. schema2class applies it to the two formats left unserved, unified on one IR.
schema2class is a Kotlin-native library that parses XSD and JSON Schema documents and programmatically generates idiomatic Kotlin source code. It is designed to be:
- Library-first: embeddable in any JVM/KMP tooling, not just a CLI
- Idiomatic Kotlin output: data classes, sealed classes, enums, nullable types, value classes
- Annotation-aware: optionally emits
@Serializable(kotlinx.serialization), Jackson, or annotation-free output - Multiplatform-ready: core IR and codegen target Kotlin Multiplatform; parsers are JVM-only today (XSD/JSON parsing depends on JVM XML/JSON libs) but the architecture leaves room to expand
- Composable: schemas can be composed, referenced, and extended. Mixed-format projects are first-class: one build can process an
.xsdand aschema.jsonside by side — e.g. a format where an XML document (XSD-described) has an element whose text content is JSON (JSON Schema-described) — generating envelope and payload classes together (schema2class-sqb)
- Parse XSD 1.0/1.1 schemas into an intermediate representation (IR)
- Parse JSON Schema (draft-07, draft-2019-09, draft-2020-12) into the same IR
- Generate idiomatic Kotlin source files from the IR
- Provide a first-class programmatic Kotlin API (not just a code generator you shell out to)
- Ship a Gradle plugin for build-time code generation
- Ship a CLI for one-off / scripted use
- Open-source under Apache 2.0
| Version | Rationale | |
|---|---|---|
| Kotlin | 1.9.x | Stable, widely deployed; K2/2.x migration is a tracked future upgrade |
| JVM toolchain | 21 | Compile with modern JDK for fast builds and toolchain access |
| JVM bytecode target | 17 | Consumers on Java 17 LTS can use the library without upgrading |
| Gradle | 8.x | Required for Configuration Cache and modern plugin APIs |
The gap between toolchain (21) and target (17) costs nothing: consumers get a library that runs on JVM 17+, and we compile it with JDK 21 to get faster incremental builds and modern toolchain resolution. A tracked issue exists for migrating to Kotlin 2.x once it is the clear ecosystem default.
- Generating Kotlin from OpenAPI/Swagger (future scope, schema layer would be reusable)
- Round-tripping generated classes back to schemas
- Generating code in languages other than Kotlin
- XSD 2.0
- Full JSON Schema vocabularies beyond the core (hyper-schema, etc.)
- Kotlin 2.x / K2 compiler (tracked for future upgrade)
Mixed-format projects (schema2class-sqb) sit above this list: a confirmed user
workflow pairs an .xsd (XML envelope) with a schema.json (payload carried as JSON
text inside one XML element). This needs no IR-level cross-format linkage — the
JSON-bearing element stays a String and the payload class is generated from its own
schema — but it does require the Gradle plugin and CLI to accept both formats in one
invocation with per-schema package configuration. It also keeps both format halves
first-class: XSD work is sequenced first, but JSON Schema is not deprioritized.
Remaining extension points, ranked by the July 2026 domain survey (docs/domain-survey.md):
- xmlutil annotation mode — pdvrieze/xmlutil is the de-facto kotlinx.serialization
XML format (KMP-ready). Emitting
@XmlSerialName/@XmlElement/@XmlValuefrom the IR'sPropertyKindmakes XSD-sourced classes actually round-trip XML on multiplatform. - WSDL types frontend — extract the XSD embedded in
<wsdl:types>and feed the existing parser. Typed SOAP payloads without generating service stubs (which remain out of scope). Today's alternative is JAXWS → Java. - DTD / RELAX NG via trang — do not write parsers for these; keep the documented conversion step (DTD/RNG/RNC → XSD → schema2class). A wrapper can be reconsidered only if users ask for it.
- OpenAPI 3.1 — embeds JSON Schema 2020-12; our JSON Schema parser is the reuse path. Fabrikt owns OpenAPI 3.0 today; no head-on competition planned.
Not pursuing: Schematron (rule-based, not structural), WADL/XML Beans/Castor (dead ecosystems).
schema2class/
├── core/ # IR definitions + codegen interfaces
│ └── src/
├── parser-xsd/ # XSD → IR (JVM, JDK built-in XML DOM — no external deps)
│ └── src/
├── parser-jsonschema/ # JSON Schema → IR (JVM, depends on Jackson)
│ └── src/
├── codegen-kotlin/ # IR → Kotlin source (pure Kotlin, KMP-ready)
│ └── src/
├── cli/ # CLI wrapper (kotlinx.cli or Clikt)
│ └── src/
├── gradle-plugin/ # Gradle plugin for build integration
│ └── src/
└── docs/ # Documentation site (mkdocs or dokka)
The IR is the central contract. Both parsers produce IR; the codegen consumes IR. This decouples schema format from output language and makes future parsers (OpenAPI, Avro, Protobuf) or future targets (TypeScript, Swift) possible.
SchemaModel (namespace, packageName, types, sourceFormat)
└── TypeDefinition (sealed)
├── ComplexType → data class
│ properties, superType (xs:extension),
│ contentProperty (xs:simpleContent text body, emitted first)
├── AliasType → typealias (value class later); carries Constraints
├── EnumType → enum class
│ EnumValue keeps serializedValue (wire) + kotlinName (constant) separate,
│ so non-identifier values like UNECE numeric codes stay round-trippable
└── UnionType → sealed class hierarchy of data class variants
PropertyDefinition → constructor property
schemaName, kotlinName, TypeRef, nullable, defaultValue, constraints
TypeRef (sealed) → Named (cross-package capable) | Primitive | ListOf
Constraint (sealed) → MinLength, MaxLength, Pattern, Min/MaxValue, Min/MaxItems
XSD complex type → data class:
// Input: XSD ComplexType "Address"
@Serializable
data class Address(
val street: String,
val city: String,
val zip: String,
val country: String? = null,
)JSON Schema oneOf → sealed class:
// Input: JSON Schema oneOf [Cat, Dog]
@Serializable
sealed class Pet {
@Serializable data class Cat(val indoor: Boolean) : Pet()
@Serializable data class Dog(val breed: String) : Pet()
}XSD simpleType restriction (enum) → enum class:
enum class Color { RED, GREEN, BLUE }- Project scaffold (Gradle multi-module, Kotlin, publishing config)
- Core IR data model
- JSON Schema parser (draft-07 baseline;
allOffollow-up tracked) - Kotlin codegen for data classes, enums, sealed classes, typealiases
- Unit test suites: schema → IR → Kotlin round-trip for both formats
(
XsdRoundTripTest,JsonSchemaRoundTripTest; real-world fixtures vendored)
- XSD parser (complex types, simple types, enumerations, sequences, attributes, simpleContent, inline anonymous types)
- XSD-specific constraints (minOccurs/maxOccurs → List/nullable, use=required → non-null)
-
xs:choice→ UnionType (whole-content choices; nested/attributed choices flatten to nullable properties),xs:group/xs:attributeGroup, element refs - Namespace → package mapping (
NamespacePackageMapper, seedocs/namespace-mapping.md) -
xs:import/xs:includemulti-file resolution (parseWithImports, one model per namespace) - XML wire namespace overrides independent from package derivation
-
oneOf/anyOf→ sealed class hierarchies (JSON Schema side) -
$refand$defsresolution — same-document, circular, and external file refs (parseWithRefs: one model per document, shared types generated once, cross-document cycles safe) -
xs:extension/xs:restrictioninheritance mapping — flattened byInheritanceFlattener(data classes are final); restriction re-declares kept content; cross-namespace chains resolved over the whole schema set - Value class generation for constrained simple types
- Default/fixed value emission — JSON Schema
default/constand XSDdefault/fixedbecome Kotlin defaults; fixed values emit guards - Friendly generated-name pipeline for JSON Schema: strategy hook, sidecar
bindings,
x-object-name/x-object-type, and sanitizedtitleconventions - Optional runtime constraint enforcement via generated
requireguards - Integer-valued enum wire values with opt-in Jackson
UNKNOWNfallback
- Gradle plugin (
schema2classGeneratetask; mixed .xsd/.json specs with per-schema package + annotation mode — seedocs/mixed-format-projects.md) - CLI (
schema2class generate -i schema.xsd -i payload.json=com.pkg -o out/; mixed formats, per-input packages, namespace overrides, annotation modes) - Source set wiring (generated dir added to the main Kotlin source set when the Kotlin JVM plugin is present)
- Generated-source drift check (
schema2classVerifyGeneratedcompares configured generated sources against a fresh build-temp generation)
- Annotation modes: none, kotlinx.serialization, xmlutil (XML), Jackson
- Configurable null omission: Jackson emits
@JsonInclude(NON_NULL); kotlinx users pair nullable defaults withJson { encodeDefaults = false } - Schema documentation carry-through to generated KDoc for types, properties, content properties, and enum values
- Dokka API docs
- Integration tests (generate → compile with real kotlinc → instantiate → JSON document round-trip via Jackson; XML document round-trips arrive with the annotation modes)
- Publishing to Maven Central
- Documentation site
License: Apache License 2.0
Rationale:
- Standard for Kotlin ecosystem tooling (kotlinx libraries, Ktor, Exposed all use Apache 2.0)
- Permissive enough for commercial use without copyleft concerns
- Patent grant protects users
- Compatible with the JVM/Gradle ecosystem's norms
- Correctly generates compilable Kotlin for 95%+ of XSD schemas encountered in practice (tested against public schemas: XHTML, Maven POM, Spring XML, etc.)
- Correctly generates compilable Kotlin for all JSON Schema draft-07 core vocabulary keywords
- Gradle plugin integrates with a standard Kotlin project in < 5 lines of config
- Generated data classes successfully serialize/deserialize real documents via kotlinx.serialization
- Published to Maven Central
- Value classes: Should constrained simple types (e.g.
xs:stringwithmaxLength) become@JvmInline value class? Ergonomic but adds boxing complexity. Tracked asschema2class-5vw. Nullability strategyResolved: nullability lives onPropertyDefinition.nullable, never onTypeRef. XSDminOccurs=0/use="optional"and JSON Schema absence-from-requiredall map tonullable = true, and codegen emits= nulldefaults for nullable properties.Kotlin package namingResolved:NamespacePackageMapperin core derives packages from namespace URIs (JAXB-style reverse-domain for http(s), ordered segments for urn), withbasePackage/overrides/defaultPackageconfig and deterministic collision suffixes. Seedocs/namespace-mapping.md.- Annotation conflicts: A field might need both a kotlinx and a Jackson annotation; should we emit both or let the user pick a mode? Current lean: annotation mode is a single codegen option (
schema2class-9oo,schema2class-n0g).