Skip to content

Latest commit

 

History

History
395 lines (329 loc) · 12.8 KB

File metadata and controls

395 lines (329 loc) · 12.8 KB

Serialization-Based Schema Generation

Table of contents

Generate JSON Schema at runtime from any @Serializable class using its SerialDescriptor.

Overview

SerializationClassJsonSchemaGenerator converts a kotlinx.serialization SerialDescriptor into a JSON Schema. It works with any @Serializable class across all supported platforms: JVM, JS/Wasm, and Native.

Use this approach when you need schema generation at runtime without a compile-time processing step, or when integrating with existing kotlinx.serialization descriptors directly.

Note

If you own the classes and target multiplatform, consider the KSP processor for zero-runtime-overhead compile-time generation.

Setup

Add the kotlinx-schema-generator-json dependency to your project:

// build.gradle.kts
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-schema-generator-json:<version>")
}

No annotation processors, plugins, or additional configuration required.

Basic Usage

Define your model using a @Serializable data class:

@Serializable
@SerialName("com.example.User")
data class User(val name: String, val age: Int)

Then generate the schema using the Default singleton:

val generator = SerializationClassJsonSchemaGenerator.Default
val schema = generator.generateSchema(User.serializer().descriptor)
println(schema.encodeToString(Json { prettyPrint = true }))

This code prints:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "com.example.User",
    "type": "object",
    "properties": {
        "name": {
            "type": "string"
        },
        "age": {
            "type": "integer"
        }
    },
    "additionalProperties": false,
    "required": [
        "name",
        "age"
    ]
}

For custom behavior, construct the generator directly with explicit introspectorConfig or jsonSchemaConfig.

Configuration

SerializationClassJsonSchemaGenerator accepts three optional constructor parameters:

Parameter Type Default Description
json Json Json { encodeDefaults = false; ... } JSON configuration, controls discriminator.
introspectorConfig SerializationClassSchemaIntrospector.Config Config() Controls how descriptors are introspected.
jsonSchemaConfig JsonSchemaConfig JsonSchemaConfig.Default Controls schema output (nullability, required).

Introspector configuration

SerializationClassSchemaIntrospector.Config controls how the generator reads descriptions from annotations on your serializable classes and properties.

public data class Config(
    val descriptionExtractor: DescriptionExtractor = DescriptionExtractor { null }
)

By default, no descriptions are extracted unless annotations are recognized by the built-in Introspections utility or a custom extractor is provided.

Custom description extraction

If your project uses a custom annotation to document properties — for example, a framework annotation or your own convention — provide a DescriptionExtractor to map it to the schema description field.

DescriptionExtractor is a functional interface:

public fun interface DescriptionExtractor {
    public fun extract(annotations: List<Annotation>): String?
}

Example: extract descriptions from a @CustomDescription annotation.

Define your annotation and model:

@OptIn(ExperimentalSerializationApi::class)
@SerialInfo
annotation class CustomDescription(val value: String)

@Serializable
@SerialName("com.example.Person")
data class Person(
    @property:CustomDescription("First name of the person")
    val firstName: String,
)

Then create a generator with a DescriptionExtractor that reads from @CustomDescription:

val generator = SerializationClassJsonSchemaGenerator(
    introspectorConfig = SerializationClassSchemaIntrospector.Config(
        descriptionExtractor = { annotations ->
            annotations.filterIsInstance<CustomDescription>().firstOrNull()?.value
        },
    ),
)

val schema: JsonSchema = generator.generateSchema(Person.serializer().descriptor)
val schemaString: String = schema.encodeToString(Json { prettyPrint = true })

println(schemaString)

This code prints:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "com.example.Person",
    "type": "object",
    "properties": {
        "firstName": {
            "type": "string",
            "description": "First name of the person"
        }
    },
    "additionalProperties": false,
    "required": [
        "firstName"
    ]
}

Tip

The extractor receives the full annotation list for each property or class. You can combine multiple annotation sources or apply fallback logic inside the lambda.

The built-in @Description annotation from kotlinx-schema-annotations is always recognized without a custom extractor. See Annotation Reference.

JSON Schema output configuration

Pass a JsonSchemaConfig to control how nullable types and required fields appear in the output:

@Serializable
@SerialName("com.example.Person")
data class Person(
    val firstName: String,
)


val generator = SerializationClassJsonSchemaGenerator(
    jsonSchemaConfig = JsonSchemaConfig.Strict,
)

val schema: JsonSchema = generator.generateSchema(Person.serializer().descriptor)
val schemaString: String = schema.encodeToString(Json { prettyPrint = true })

println(schemaString)

This code generates:

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "com.example.Person",
    "type": "object",
    "properties": {
        "firstName": {
            "type": "string"
        }
    },
    "additionalProperties": false,
    "required": [
        "firstName"
    ]
}

JsonSchemaConfig presets

Preset Description
Default Respects default values; nullable fields use union types ["string", "null"].
Strict All fields required (including nullable); union types. Use for OpenAI strict function calling.
OpenAPI Nullable fields use "nullable": true; includes discriminator for polymorphic types.

JsonSchemaConfig reference

Property Type Default Description
respectDefaultPresence Boolean false Mark fields with default values as optional (requires reflection).
requireNullableFields Boolean true Whether nullable fields appear in the required array.
useUnionTypes Boolean true Represent nullable types as ["string", "null"] (Draft 2020-12).
useNullableField Boolean false Emit "nullable": true instead of union types (legacy OpenAPI compatibility).
includeDiscriminator Boolean false Include a discriminator object in polymorphic schemas (OpenAPI 3.x).

Note

useUnionTypes and useNullableField are mutually exclusive — exactly one must be true.

Polymorphic types

Sealed classes are supported. The generator reads the discriminator configuration from the Json instance you provide:

@Serializable
@SerialName("com.example.Shape")
sealed class Shape {
    @Serializable
    @SerialName("com.example.Shape.Circle") 
    data class Circle(val radius: Double) : Shape()
    
    @Serializable
    @SerialName("com.example.Shape.Rectangle") 
    data class Rectangle(val width: Double, val height: Double) : Shape()
}
val generator = SerializationClassJsonSchemaGenerator(
    json = Json { classDiscriminator = "type" }
)

val schema: JsonSchema = generator.generateSchema(Shape.serializer().descriptor)
val schemaString: String = schema.encodeToString(Json { prettyPrint = true })

println(schemaString)

The generated schema uses oneOf with a $defs section for each subtype, with a required type discriminator field on each subtype object.

{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "com.example.Shape",
    "type": "object",
    "additionalProperties": false,
    "oneOf": [
        {
            "$ref": "#/$defs/com.example.Shape.Circle"
        },
        {
            "$ref": "#/$defs/com.example.Shape.Rectangle"
        }
    ],
    "$defs": {
        "com.example.Shape.Circle": {
            "type": "object",
            "properties": {
                "radius": {
                    "type": "number"
                }
            },
            "required": [
                "radius"
            ],
            "additionalProperties": false
        },
        "com.example.Shape.Rectangle": {
            "type": "object",
            "properties": {
                "width": {
                    "type": "number"
                },
                "height": {
                    "type": "number"
                }
            },
            "required": [
                "width",
                "height"
            ],
            "additionalProperties": false
        }
    }
}

See Also