๐Ÿ˜ธ kt-schema

Configuration

Table of contents

Schema configuration

The me.kpavlov.kt.schema.config processor option selects the schema flavour for both KSP and the Java annotation processor. Unset, classes use strict and KSP function schemas use FunctionCallingSchemaConfig.Strict. The value is a case-insensitive shortcut:

ValueSchema
strictAll fields required, nullable types as ["string", "null"], additionalProperties: false.
lenientCompact and permissive: only non-nullable fields without defaults are required, no null markers, extra properties allowed.
openapiOpenAPI 3.x flavour: nullable types use "nullable": true.
kotlin
// KSP
ksp {
    arg("me.kpavlov.kt.schema.config", "lenient")
}

// Java annotation processor
tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.add("-Ame.kpavlov.kt.schema.config=lenient")
}

Instead of a shortcut, the value can be the fully qualified name of your own JsonSchemaConfig class: a Kotlin object, or a class with a public no-arg constructor. It must be on the processor classpath, i.e. provided by a ksp(...) / annotationProcessor(...) dependency. For function schemas (KSP), a plain JsonSchemaConfig is copied into a FunctionCallingSchemaConfig with strictMode = false. An unresolvable value fails the build with an error listing the valid shortcuts.

important

lenient schemas express null as an absent field: a nullable property has no null marker and is never required. Nullable collection elements and map values can’t be omitted, so they keep an anyOf branch with {"type": "null"}. Serialize payloads with explicitNulls = false (e.g. Json { explicitNulls = false }), otherwise an explicit null does not validate.

Multi-Framework Annotation Support

You don’t need to change your existing code!

kt-schema recognizes description annotations from multiple frameworks by their simple name, allowing you to generate schemas from code that uses annotations from other libraries.

Supported Annotations

The library automatically recognizes these description annotations by default:

AnnotationSimple NameLibrary/FrameworkExample
me.kpavlov.kt.schema.DescriptionDescriptionkt-schema@Description("User name")
ai.koog.agents.core.tools.annotations.LLMDescriptionLLMDescriptionKoog AI agents@LLMDescription("Query text")
com.fasterxml.jackson.annotation.JsonPropertyDescriptionJsonPropertyDescriptionJackson@JsonPropertyDescription("Email")
com.fasterxml.jackson.annotation.JsonClassDescriptionJsonClassDescriptionJackson@JsonClassDescription("User model")
dev.langchain4j.model.output.structured.PPLangChain4j@P("Search query")

Beyond descriptions, kt-schema also recognizes name overrides and ignore markers by default:

Name overrides โ€” matched by fully qualified name (case-sensitive):

AnnotationMaps to
com.fasterxml.jackson.annotation.JsonPropertyproperty name in properties/required
com.fasterxml.jackson.annotation.JsonTypeNamepolymorphic subtype name ($defs key + discriminator const)

Ignore markers โ€” matched by simple name (case-insensitive), regardless of package:

AnnotationMaps to
JsonIgnore (any package)excludes the property/field from the schema

Default values โ€” matched by fully qualified name (case-sensitive); mainly useful for the KSP and Java APT processors, which can’t evaluate a real Kotlin/Java default-value expression at compile time:

AnnotationMaps to
com.fasterxml.jackson.annotation.JsonEnumDefaultValuethe enum constant it’s placed on becomes that enum type’s default (on the enum’s own schema, in $defs)
com.fasterxml.jackson.annotation.JsonProperty(defaultValue=..)the property’s default value, internally
note

The @JsonEnumDefaultValue-derived default always appears in the generated output, since it’s a property of the enum’s own schema. @JsonProperty(defaultValue = "...") is recognized and populates the property internally, but doesn’t appear in โ€” or exclude the property from required in โ€” the KSP/APT processors’ standard generated resource, since both mark every property required regardless of default presence. It does apply when building a schema through TypeGraphToJsonSchemaTransformer with a non-strict JsonSchemaConfig yourself.

How It Works

Each annotation category (description, name-override, ignore) is configured with its own list of recognized names. Within a list, entries containing a dot are matched case-sensitively against the fully qualified name; entries without a dot are matched case-insensitively against the simple name. This means:

note

Multi-framework annotation recognition applies to the KSP processor, the Java APT processor (via AptIntrospectionContext, which recognizes the same Jackson defaults), and reflection-based generators. The serialization-based generator (SerializationClassJsonSchemaGenerator) can only access annotations marked with @SerialInfo โ€” see Custom description extraction for details.

Customizing Annotation Detection

Annotation detection is configurable via kt-schema.properties loaded from the classpath. The configuration file is optional โ€” if not provided or fails to load, the library uses sensible defaults.

Default Configuration

By default, the library recognizes:

Description annotations: Description, LLMDescription, JsonPropertyDescription, JsonClassDescription, P Description attributes: value, description Ignore annotations: SchemaIgnore, SerialSchemaIgnore, JsonIgnoreType, JsonIgnore Name-override annotations: kotlinx.serialization.SerialName, com.fasterxml.jackson.annotation.JsonProperty, com.fasterxml.jackson.annotation.JsonTypeName Name-override attributes: value Enum-default annotations: com.fasterxml.jackson.annotation.JsonEnumDefaultValue Default-value annotations: com.fasterxml.jackson.annotation.JsonProperty Default-value attributes: defaultValue Discriminator annotations: kotlinx.serialization.json.JsonClassDiscriminator, com.fasterxml.jackson.annotation.JsonTypeInfo Discriminator attributes: discriminator, property

note

Annotation names containing a dot (e.g., kotlinx.serialization.SerialName) are matched case-sensitively against the annotation’s fully qualified name. Names without a dot are matched case-insensitively by simple name.

Adding Custom Annotations

To customize, place kt-schema.properties in your project’s resources:

properties
# Add your custom annotations to the defaults
introspector.annotations.description.names=Description,MyCustomAnnotation,DocString
introspector.annotations.description.attributes=value,description,text
# Name-override annotations (use FQN for precise matching)
introspector.annotations.name.names=kotlinx.serialization.SerialName
introspector.annotations.name.attributes=value
# Default-value annotations (use FQN for precise matching)
introspector.annotations.enumDefault.names=com.fasterxml.jackson.annotation.JsonEnumDefaultValue
introspector.annotations.defaultValue.names=com.fasterxml.jackson.annotation.JsonProperty
introspector.annotations.defaultValue.attributes=defaultValue
# Validation constraint annotations, mapped by simple name (Min, Max, Size, Pattern, ...)
introspector.annotations.constraint.names=jakarta.validation.constraints.*,javax.validation.constraints.*
# Polymorphic discriminator property name (reflection and KSP front ends; defaults to "type")
introspector.annotations.discriminator.names=kotlinx.serialization.json.JsonClassDiscriminator,com.fasterxml.jackson.annotation.JsonTypeInfo
introspector.annotations.discriminator.attributes=discriminator,property

Note: The library falls back to built-in defaults if the configuration file is missing or cannot be loaded.

Example: Adding Support for a Custom Framework

kotlin
// Your custom annotation
package com.mycompany.annotations

annotation class ApiDoc(val text: String)

// Usage in your models
@ApiDoc(text = "Customer profile information")
data class Customer(
    @ApiDoc(text = "Unique customer identifier")
    val id: Long,
    val name: String
)

Update kt-schema.properties:

properties
introspector.annotations.description.names=Description,ApiDoc
introspector.annotations.description.attributes=value,description,text

Now the schema generator will recognize @ApiDoc and extract descriptions from its text parameter.

Example: Reusing Jackson Annotations

If your project already uses Jackson for JSON serialization, you can generate schemas from existing Jackson-annotated classes without any modifications. This is particularly useful for REST APIs and Spring Boot applications where Jackson annotations are already present.

kotlin
// Existing code with Jackson annotations - NO CHANGES NEEDED!
@JsonClassDescription("Customer profile data")
data class Customer(
    @JsonPropertyDescription("Unique customer ID")
    val id: Long,

    @JsonPropertyDescription("Full name")
    val name: String,

    @JsonPropertyDescription("Contact email")
    val email: String
)

// Generate JSON schema without modifying the code
val generator = me.kpavlov.kt.schema.generator.json.ReflectionClassJsonSchemaGenerator.Default
val schema = generator.generateSchema(Customer::class)

// Schema includes all Jackson descriptions!

Example: LangChain4j Integration

LangChain4j uses the @P annotation for parameter descriptions in AI function calling. The library recognizes these annotations automatically, enabling seamless integration with existing LangChain4j codebases.

kotlin
// Code using LangChain4j annotations
data class SearchQuery(
    @P("Search terms")
    val query: String,

    @P("Maximum results to return")
    val limit: Int = 10
)

// Generate schema for LLM function calling
val generator = ReflectionFunctionCallingSchemaGenerator.Default
val schema = generator.generateSchema(SearchQuery::class.constructors.first())

Example: Koog AI Agents

Koog AI framework uses @LLMDescription for documenting agent tools and parameters. The library supports both the verbose description = syntax and the shorthand form, making migration from Koog straightforward.

kotlin
@LLMDescription(description = "Product with pricing information")
@Schema
data class Product(
    @LLMDescription(description = "Product identifier")
    val id: Long,

    @LLMDescription("Product name")
    val name: String,

    @LLMDescription("Unit price")
    val price: Double,
)

Precedence Rules

If multiple description annotations are present on the same element, the first matching annotation in source-code order wins. There is no special priority for @Description over other recognized annotations โ€” whichever recognized annotation appears first on the declaration is used.

Tip: For predictability, place only one description annotation per element. If you use @Description from kotlinx-schema alongside another recognized annotation, put @Description first in source order to ensure it wins.

See Also