๐Ÿ˜ธ kt-schema

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):

json
{
    "$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"
    ]
}

Examples

Basic data classes

Here’s a practical example of a product model with various property types:

kotlin
@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:

kotlin
val schema = Product::class.jsonSchemaString
val schemaObject = Product::class.jsonSchema
Generated JSON schema
json
{
    "$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:

kotlin
@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
json
{
    "$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:

kotlin
@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:

kotlin
@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:

kotlin
@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()
}
kotlin
val generator = ReflectionClassJsonSchemaGenerator.Default
val schema = generator.generateSchema(Animal::class)

println(schema.encodeToString(Json { prettyPrint = true }))
Generated JSON schema
json
{
    "$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:

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:

kotlin
@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):

kotlin
@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:

properties
introspector.annotations.ignore.names=SchemaIgnore,SerialSchemaIgnore,JsonIgnoreType,MyCustomIgnore

Using @Schema and @Description annotations

@Schema annotation

Mark classes with @Schema to generate extension properties for them:

kotlin
@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:

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:

kotlin
@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.