# kotlinx-schema-json Kotlin Multiplatform library providing type-safe models and DSL for building JSON Schema definitions with serialization support. ## Features - **Type-safe data models** for [JSON Schema Draft 2020-12](https://json-schema.org/draft/2020-12) - **Kotlin DSL** for declarative schema construction - **Kotlinx Serialization** integration for JSON serialization/deserialization - **Property types**: string, number, integer, boolean, array, object, reference - **Polymorphism**: `oneOf`, `anyOf`, `allOf` with discriminator support for elegant type unions - **Constraints**: required fields, additional properties, min/max, enum, const, nullable - **Nested schemas** with full object and array support ## Installation ```kotlin dependencies { implementation("org.jetbrains.kotlinx:kotlinx-schema-json:$version") } ``` For JVM-only project use [org.jetbrains.kotlinx:kotlinx-schema-json-jvm](https://central.sonatype.com/artifact/org.jetbrains.kotlinx/kotlinx-schema-json-jvm) dependency. ## Usage ### Building schemas with DSL The DSL provides a type-safe way to build JSON Schema definitions. #### Basic Example ```kotlin val schema = jsonSchema { name = "UserEmail" strict = false schema { property("email") { required = true string { description = "Email address" format = "email" } } } } ``` Produces: ```json { "name": "UserEmail", "strict": false, "schema": { "type": "object", "properties": { "email": { "type": "string", "description": "Email address", "format": "email" } }, "required": [ "email" ] } } ``` #### Comprehensive Example This example demonstrates all available DSL features in a single schema:
Click to expand... ```kotlin val schema = jsonSchema { name = "UserProfile" strict = true description = "Complete user profile schema" schema { // Schema metadata id = "https://example.com/schemas/user-profile" schema = "https://json-schema.org/draft-07/schema" additionalProperties = false // Required string with format constraint and description property("id") { required = true string { format = "uuid" description = "Unique identifier" } } // Required string with format and length constraints property("email") { required = true string { format = "email" description = "Email address" minLength = 5 maxLength = 100 } } // Integer with numeric constraints property("age") { integer { description = "Person's age" minimum = 0.0 maximum = 150.0 } } // Number with multipleOf constraint property("score") { number { description = "User score" minimum = 0.0 maximum = 100.0 multipleOf = 0.5 } } // String enum property("status") { string { description = "Current status" enum = listOf("active", "inactive", "pending") } } // Boolean with a default value property("verified") { boolean { description = "Email verification status" default = false } } // Nullable property property("nickname") { string { description = "Optional nickname" nullable = true } } // Constant value property("apiVersion") { string { description = "API version" constValue = "v1.0" } } // Array of strings with size constraints property("tags") { array { description = "User tags" minItems = 1 maxItems = 10 ofString() } } // Nested object with required fields property("metadata") { obj { description = "User metadata" property("createdAt") { required = true string { format = "date-time" } } property("updatedAt") { string { format = "date-time" } } } } // Array of objects property("activities") { array { description = "User activity log" ofObject { additionalProperties = false property("action") { required = true string { description = "Action performed" } } property("timestamp") { required = true string { format = "date-time" description = "When action occurred" } } } } } // Schema reference property("address") { reference("#/definitions/Address") } } } ```
This comprehensive example includes: - **Required fields**: `id`, `email` (with `required = true`) - **String constraints**: `format`, `minLength`, `maxLength`, `enum`, `pattern` - **Numeric constraints**: `minimum`, `maximum`, `multipleOf`, `exclusiveMinimum`, `exclusiveMaximum` - **Default values**: Direct assignment `default = value` (automatically wrapped in `JsonPrimitive`) - **Nullable properties**: Using `nullable = true` - **Constant values**: Direct assignment `constValue = value` (automatically wrapped in `JsonPrimitive`) - **Arrays**: With `minItems`, `maxItems`, and typed `items` - **Nested objects**: Using `obj { }` with nested properties - **Array of objects**: Complex array items with their own schemas - **Schema references**: Using `reference()` for reusable schemas - **Schema metadata**: `$id`, `$schema`, `strict`, `description`, `additionalProperties` ### Serialization and deserialization ```kotlin import kotlinx.serialization.json.Json val json = Json { prettyPrint = true } // Serialize to JSON string val jsonString = json.encodeToString(userSchema) // Deserialize from JSON string val schema = json.decodeFromString(jsonString) ``` ### Working with nested objects ```kotlin val productSchema = jsonSchema { name = "Product" schema { property("metadata") { obj { description = "Product metadata" property("createdAt") { required = true string { format = "date-time" } } property("updatedAt") { string { format = "date-time" } } } } } } ``` ### Polymorphism with oneOf, anyOf, allOf JSON Schema supports polymorphic types through composition keywords. These enable flexible type definitions where a value can match one or more schemas. #### oneOf - Exactly One Match Use `oneOf` when a value must match exactly one of the provided schemas: ```kotlin val schema = jsonSchema { name = "FlexibleValue" schema { property("value") { required = true oneOf { description = "String or number" string { minLength = 1 } number { minimum = 0.0 } } } } } ``` #### anyOf - One or More Matches Use `anyOf` when a value must match at least one of the provided schemas: ```kotlin val schema = jsonSchema { name = "IdSchema" schema { property("id") { required = true anyOf { description = "UUID or integer ID" string { format = "uuid" } integer { minimum = 1.0 } } } } } ``` #### allOf - All Must Match Use `allOf` for schema composition where a value must match all provided schemas: ```kotlin val schema = jsonSchema { name = "AdminUserSchema" schema { property("user") { required = true allOf { description = "Admin user extends base user" reference("#/definitions/BaseUser") obj { property("role") { required = true string { enum = listOf("admin", "superadmin") } } } } } } } ``` #### Discriminators for Efficient Type Resolution Discriminators enable efficient polymorphic type resolution by specifying which property determines the schema to use. The DSL provides two elegant forms: **Form 1: References with discriminator mapping** ```kotlin val schema = jsonSchema { name = "PetSchema" schema { property("pet") { required = true oneOf { discriminator(propertyName = "petType") { "dog" mappedTo "#/definitions/Dog" "cat" mappedTo "#/definitions/Cat" } // Both mapping and references added automatically! } } } } ``` **Form 2: Inline schemas (concise)** ```kotlin val schema = jsonSchema { name = "PaymentSchema" schema { property("payment") { required = true oneOf { discriminator(propertyName = "type") { "credit_card" mappedTo { property("type") { required = true string { constValue = "credit_card" } } property("cardNumber") { required = true string() } } "paypal" mappedTo { property("type") { required = true string { constValue = "paypal" } } property("email") { required = true string { format = "email" } } } } } } } } ``` **Mixing references and inline schemas:** ```kotlin oneOf { discriminator(propertyName = "kind") { "external" mappedTo "#/definitions/ExternalType" // Reference "inline" mappedTo { // Inline schema property("kind") { string { constValue = "inline" } } property("data") { string() } } } } ``` The `mappedTo` infix operator automatically: - **For references**: Adds entry to discriminator mapping AND adds reference to oneOf options - **For inline schemas**: Adds schema to oneOf options (no explicit mapping needed) This eliminates duplication - you only specify each mapping once! #### Nested Polymorphism Polymorphic types can be nested inside each other: ```kotlin property("complex") { allOf { reference("#/definitions/Base") oneOf { string { description = "String variant" } integer { description = "Integer variant" } } } } ``` ## API Overview ### Property Types - `string { }` - String properties with `format`, `enum`, `pattern`, `minLength`, `maxLength` - `integer { }` - Integer properties with constraints - `number { }` - Numeric properties with constraints - `boolean { }` - Boolean properties - `array { }` - Array properties with `items` definition - `obj { }` - Nested object properties - `reference(ref)` - Schema references (`$ref`) - `oneOf { }` - Exactly one schema must match (with optional discriminator) - `anyOf { }` - One or more schemas must match - `allOf { }` - All schemas must match ### Constraints - `required = true` - Mark a property as required (set within the property block) - `nullable = true` - Allow null values - `default = value` - Set default value (automatically wrapped in `JsonPrimitive`, or use `JsonElement` directly) - `constValue = value` - Set constant value (automatically wrapped in `JsonPrimitive`, or use `JsonElement` directly) - `enum = listOf(...)` - Enumerate allowed values - `minimum`, `maximum` - Numeric bounds - `minLength`, `maxLength` - String length constraints - `minItems`, `maxItems` - Array size constraints - `additionalProperties` - Allow/disallow additional properties **Note**: The legacy `required(vararg fields: String)` function is still available for backward compatibility, but using `required = true` within property blocks is the recommended approach. ## DSL Safety The DSL uses Kotlin's `@DslMarker` annotation to prevent scope pollution and ensure type-safe schema construction. Builder classes have internal constructors, enforcing DSL usage through the `jsonSchema { }` entry point. ## Function Calling Schema for LLM APIs For LLM function calling APIs (OpenAI, Anthropic), use `FunctionCallingSchema` to represent function/tool definitions: ```kotlin import kotlinx.schema.json.* val schema = FunctionCallingSchema( name = "search", description = "Search for items in the database", parameters = ParametersDefinition( properties = mapOf( "query" to StringPropertyDefinition( type = listOf("string"), description = "Search query" ), "limit" to NumericPropertyDefinition( type = listOf("integer"), description = "Max results" ) ), required = listOf("query", "limit") ) ) ``` This produces the following JSON schema compatible with OpenAI's function calling format: ```json { "type": "function", "name": "search", "description": "Search for items in the database", "strict": true, "parameters": { "type": "object", "properties": { "query": { "type": "string", "description": "Search query" }, "limit": { "type": "integer", "description": "Max results" } }, "required": [ "query", "limit" ], "additionalProperties": false } } ``` ### OpenAI Structured Outputs Compatibility Function calling schemas follow OpenAI's [structured outputs requirements](https://platform.openai.com/docs/guides/function-calling): - Functions must have a `name` and `description` - All fields must be in the `required` array - Nullable fields use union types: `["string", "null"]` - The `nullable: true` flag is not used (it's ignored by OpenAI models) - `additionalProperties` is set to `false` by default Example with nullable parameter: ```kotlin val schema = FunctionCallingSchema( name = "updateProfile", description = "Update user profile information", parameters = ParametersDefinition( properties = mapOf( "name" to StringPropertyDefinition( type = listOf("string"), description = "User's full name" ), "bio" to StringPropertyDefinition( type = listOf("string", "null"), description = "Optional biography" ) ), required = listOf("name", "bio") ) ) ``` ### Runtime Generation from Functions For generating function calling schemas from Kotlin functions at runtime, use `ReflectionFunctionCallingSchemaGenerator` in the `kotlinx-schema-generator-json` module: ```kotlin import kotlinx.schema.Description import kotlinx.schema.generator.json.ReflectionFunctionCallingSchemaGenerator @Description("Search for users by name") fun searchUsers( @Description("Name to search for") query: String, @Description("Maximum number of results") limit: Int = 10 ): List = TODO() val generator = ReflectionFunctionCallingSchemaGenerator.Default val schema = generator.generateSchema(::searchUsers) ``` This automatically generates a `FunctionCallingSchema` with the function name, description, and properly typed parameters: ```json { "type": "function", "name": "searchUsers", "description": "Search for users by name", "strict": true, "parameters": { "properties": { "query": { "type": "string", "description": "Name to search for" }, "limit": { "type": "integer", "description": "Maximum number of results" } }, "required": [ "query", "limit" ], "additionalProperties": false, "type": "object" } } ``` ### Sealed Class Polymorphic Schema Generation The library automatically generates JSON schemas for Kotlin sealed class hierarchies using `oneOf` with discriminator support. This is perfect for representing polymorphic types in APIs and validation. #### Basic Sealed Class Example ```kotlin import kotlinx.schema.Description import kotlinx.schema.generator.json.ReflectionClassJsonSchemaGenerator @Description("Represents an animal") sealed class Animal { @Description("Animal's name") abstract val name: String @Description("Represents a dog") data class Dog( override val name: String, @property:Description("Dog's breed") val breed: String, @property:Description("Trained or not") val isTrained: Boolean = false, ) : Animal() @Description("Represents a cat") data class Cat( override val name: String, @property:Description("Cat's color") val color: String, @property:Description("Lives left") val lives: Int = 9, ) : Animal() } val generator = ReflectionClassJsonSchemaGenerator.Default val schema = generator.generateSchema(Animal::class) ``` This generates a JSON schema with `oneOf` and `$ref`/`$defs` for the sealed hierarchy: ```json { "name": "kotlinx.schema.generator.json.JsonSchemaHierarchyTest.Animal", "strict": false, "schema": { "type": "object", "additionalProperties": false, "description": "Represents an animal", "oneOf": [ { "$ref": "#/$defs/Cat" }, { "$ref": "#/$defs/Dog" } ], "discriminator": { "propertyName": "type", "mapping": { "Cat": "#/$defs/Cat", "Dog": "#/$defs/Dog" } }, "$defs": { "Cat": { "type": "object", "description": "Represents a cat", "properties": { "name": { "type": "string", "description": "Animal's name" }, "color": { "type": "string", "description": "Cat's color" }, "lives": { "type": "integer", "description": "Lives left" } }, "required": [ "name", "color" ], "additionalProperties": false }, "Dog": { "type": "object", "description": "Represents a dog", "properties": { "name": { "type": "string", "description": "Animal's name" }, "breed": { "type": "string", "description": "Dog's breed" }, "isTrained": { "type": "boolean", "description": "Trained or not" } }, "required": [ "name", "breed" ], "additionalProperties": false } } } } ``` #### Key Features - **Automatic `oneOf` generation**: Each sealed subclass becomes an alternative in the `oneOf` array with `$ref`pointers - **`$defs` section**: Subclass schemas are defined in the `$defs` section and referenced via `$ref` - **Discriminator support**: Automatically generates discriminator with explicit type mapping to `$ref` paths - **Property inheritance**: Properties from the sealed base class are included in each subtype - **Optional properties**: Properties with default values are correctly marked as optional - **Documentation**: `@Description` annotations are preserved in the schema - **Type safety**: Ensures each subtype has a unique schema structure #### Use Cases Sealed class schemas are ideal for: - **API payloads**: Representing different message or event types - **Configuration**: Defining different configuration variants - **State machines**: Modeling different states with specific properties - **Domain modeling**: Expressing algebraic data types in JSON Schema - **Validation**: Ensuring polymorphic data matches one of the allowed types ## Conformance testing kotlinx-schema-json aims to maintain full conformance with the JSON Schema specification. Conformance tests against the official [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) are to ensure compatibility and correctness. Our goal is to support all features of the specification while providing a robust and reliable JSON Schema implementation for Kotlin.