Generated Schemas
Table of contents
What Gets Generated
Schemas follow JSON Schema Draft 2020-12 format. Here is what KSP emits with its default strict config for the
Address model from the Quick Start (pretty-printed):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "com.example.Address",
"description": "A postal address for deliveries and billing.",
"type": "object",
"properties": {
"street": {
"type": "string",
"description": "Street address, including house number",
"maxLength": 120
},
"city": {
"type": "string",
"description": "City or town name",
"minLength": 1,
"maxLength": 80
},
"zipCode": {
"type": "string",
"description": "Postal or ZIP code",
"pattern": "[0-9]{5}"
},
"country": {
"type": "string",
"description": "Two-letter ISO country code; defaults to US",
"pattern": "[A-Z]{2}"
}
},
"additionalProperties": false,
"required": [
"street",
"city",
"zipCode",
"country"
]
}- Enums are
type: stringwithenum: []and carry@Descriptionasdescription. - Object properties include their inferred type schema and, when present, property-level
@Descriptionasdescription. - Default values are automatically extracted and included in the schema when using runtime reflection (e.g.,
val country: String = "US"โ"default": "US";val note: String? = nullโ"default": null). KSP (compile-time) records that a property has a default but cannot extract its value, so it emits nodefault. A property is optional when it has a Kotlin default or an explicit optional marker; a known default also makes it non-required, and it is emitted asdefaultonly for non-required properties. - Nullable properties are emitted as a union including
null. - Collections:
List<T>/Set<T>โ{ "type":"array", "items": T };Map<String, V>โ{ "type":"object", "additionalProperties": V }. - Validation constraints: Jakarta Bean Validation annotations (
@Min,@Size,@Pattern,@Email, โฆ) and built-in type ranges (Byte,UInt,Char) and formats (UUID,Instant,LocalDate) become JSON Schema keywords โ see the Validation Constraints guide . kotlin.Any/ unbound generic type parameters (e.g.,T) map to the empty schema{}, which accepts any JSON value.- Named types (nested objects, enums, sealed classes) are deduplicated in a
$defssection and referenced via$refat every usage site.
Examples
Basic data classes
Here’s a practical example of a product model with various property types:
@Description("A purchasable product with pricing and inventory info.")
@Schema
data class Product(
@Description("Unique identifier for the product")
val id: Long,
@Description("Human-readable product name")
val name: String,
@Description("Optional detailed description of the product")
val description: String?,
@Description("Unit price expressed as a decimal number")
val price: Double,
@Description("Whether the product is currently in stock")
val inStock: Boolean = true,
@Description("List of tags for categorization and search")
val tags: List<String> = emptyList(),
)Use the generated extensions:
val schema = Product::class.jsonSchemaString
val schemaObject = Product::class.jsonSchemaGenerated JSON schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "com.example.Product",
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Unique identifier for the product"
},
"name": {
"type": "string",
"description": "Human-readable product name"
},
"description": {
"type": [
"string",
"null"
],
"description": "Optional detailed description of the product"
},
"price": {
"type": "number",
"description": "Unit price expressed as a decimal number"
},
"inStock": {
"type": "boolean",
"description": "Whether the product is currently in stock"
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of tags for categorization and search"
}
},
"required": [
"id",
"name",
"description",
"price",
"inStock",
"tags"
],
"additionalProperties": false,
"description": "A purchasable product with pricing and inventory info."
}Enums with descriptions
Enums are supported with descriptions on both the enum class and individual values:
@Description("Current lifecycle status of an entity.")
@Schema
enum class Status {
@Description("Entity is active and usable")
ACTIVE,
@Description("Entity is inactive or disabled")
INACTIVE,
@Description("Entity is pending activation or approval")
PENDING,
}Generated JSON schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "com.example.Status",
"type": "string",
"enum": [
"ACTIVE",
"INACTIVE",
"PENDING"
],
"description": "Current lifecycle status of an entity."
}Nested objects
You can compose schemas by nesting annotated classes:
@Description("A person with a first and last name and age.")
@Schema
data class Person(
@Description("Given name of the person")
val firstName: String,
@Description("Family name of the person")
val lastName: String,
@Description("Age of the person in years")
val age: Int,
)
@Description("An order placed by a customer containing multiple items.")
@Schema
data class Order(
@Description("Unique order identifier")
val id: String,
@Description("The customer who placed the order")
val customer: Person,
@Description("Destination address for shipment")
val shippingAddress: Address,
@Description("List of items included in the order")
val items: List<Product>,
@Description("Current status of the order")
val status: Status,
)The generated schema for Order will automatically include definitions for all nested types (Person, Address,
Product, Status) in the $defs section, with appropriate $ref pointers to link them together. This makes it easy
to build complex, composable data models.
Generic types
Generic classes are supported, with type parameters resolved at usage sites:
@Description("A generic container that wraps content with optional metadata.")
@Schema
data class Container<T>(
@Description("The wrapped content value")
val content: T,
@Description("Arbitrary metadata key-value pairs")
val metadata: Map<String, Any> = emptyMap(),
)Generic type parameters are resolved at the usage site. Unbound type parameters (like T), star projections and
kotlin.Any-typed properties map to {} โ the empty JSON Schema that accepts any JSON value. For more specific typing,
instantiate the generic class with concrete types when you need them.
Each application of a generic class gets its own definition, named after its type arguments: Box<String> and
Box<Int> in one schema become Box_of_String and Box_of_Int, Pair<String, Int> becomes Pair_of_String_and_Int
and Box<String?> becomes Box_of_nullable_String. If the argument names clash, package segments are added. With
kotlinx.serialization a class is named this way only when it is used with several type arguments, after the
properties whose types differ, as a descriptor can’t tell type arguments from other properties.
Sealed class polymorphism
The library automatically generates JSON schemas for Kotlin sealed class hierarchies using oneOf:
@Description("Multicellular eukaryotic organism of the kingdom Metazoa")
@Schema
sealed class Animal {
/**
* Animal's name
*/
@Description("Animal's name")
abstract val name: String
@Schema(withSchemaObject = true)
data class Dog(
@Description("Animal's name")
override val name: String,
) : Animal()
// @SerialName overrides the emitted type name for this subtype only (see below)
@SerialName("Cat")
@Schema(withSchemaObject = true)
data class Cat(
@Description("Animal's name")
override val name: String,
) : Animal()
}val generator = ReflectionClassJsonSchemaGenerator.Default
val schema = generator.generateSchema(Animal::class)
println(schema.encodeToString(Json { prettyPrint = true }))Generated JSON schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "me.kpavlov.kt.schema.integration.type.Animal",
"description": "Multicellular eukaryotic organism of the kingdom Metazoa",
"type": "object",
"oneOf": [
{
"$ref": "#/$defs/Cat"
},
{
"$ref": "#/$defs/me.kpavlov.kt.schema.integration.type.Animal.Dog"
}
],
"$defs": {
"Cat": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "Cat"
},
"name": {
"type": "string",
"description": "Animal's name"
}
},
"required": [
"type",
"name"
],
"additionalProperties": false
},
"me.kpavlov.kt.schema.integration.type.Animal.Dog": {
"type": "object",
"properties": {
"type": {
"type": "string",
"const": "me.kpavlov.kt.schema.integration.type.Animal.Dog"
},
"name": {
"type": "string",
"description": "Animal's name"
}
},
"required": [
"type",
"name"
],
"additionalProperties": false
}
}
}Key features:
oneOfwith$ref: Each sealed subclass is stored in$defsand referenced via$ref- Fully qualified names by default:
$defskeys and discriminatorconstvalues use fully qualified class names (e.g.,com.example.Animal.Dog) to avoid collisions across packages - Name overrides: a subtype annotated with
@SerialName/@JsonTypeName(or another recognized name-override annotation) uses that short name instead of its FQN โ likeCatabove, overridden via@SerialName("Cat"). Two declarations with the same explicit name fail schema generation instead of being renamed - Discriminator property: A
typefield with aconstvalue is automatically added to each subtype for runtime dispatch. The reflection and KSP front ends take the name from@JsonClassDiscriminator("...")or@JsonTypeInfo(property = "...")on the sealed type or its supertypes; a subtype declaring a property with that name fails schema generation - Property inheritance: Base class properties are included in each subtype
- Type safety: Each subtype gets its own schema definition
Excluding subtypes with @SchemaIgnore
Use @SchemaIgnore to exclude specific sealed subtypes from the generated schema.
The class remains fully functional at runtime โ only schema generation is affected:
@Schema
sealed class Event {
data class Click(val x: Int, val y: Int) : Event()
@SchemaIgnore
data class Internal(val trace: String) : Event()
}The Internal subtype will not appear in the oneOf composition or $defs.
For serialization-based generation, use @SerialSchemaIgnore (which carries @SerialInfo):
@Serializable
sealed class Event {
@Serializable
data class Click(val x: Int, val y: Int) : Event()
@Serializable
@SerialSchemaIgnore
data class Internal(val trace: String) : Event()
}Jackson’s @JsonIgnoreType is also recognized automatically. Custom ignore annotations can be
registered via kt-schema.properties:
introspector.annotations.ignore.names=SchemaIgnore,SerialSchemaIgnore,JsonIgnoreType,MyCustomIgnoreUsing @Schema and @Description annotations
@Schema annotation
Mark classes with @Schema to generate extension properties for them:
@Schema // Uses default schema type "json"
data class Address(val street: String, val city: String)
@Schema("json") // Explicitly specify schema type
data class Person(val name: String, val age: Int)@Schema parameters:
value = "json": Schema type (only JSON currently supported)withSchemaObject = false: GeneratejsonSchema: JsonObjectproperty (see Advanced Configuration )
Note: jsonSchemaString is always generated. jsonSchema requires withSchemaObject = true.
@Description annotation
Use @Description on classes and properties to add human-readable documentation to your schemas:
@Description("A purchasable product with pricing info")
@Schema
data class Product(
@Description("Unique identifier for the product") val id: Long,
@Description("Human-readable product name") val name: String,
@Description("Optional detailed description of the product") val description: String?,
@Description("Unit price expressed as a decimal number") val price: Double,
)Tip: With the recommended compiler flag -Xannotation-default-target=param-property, a bare @Description on a
primary constructor parameter also applies to the property. If you do not enable the flag, use @param:Description for
constructor-declared properties.