Skip to content

Commit

Permalink
add suppressDescription flag to JsonSchemaGenerator
Browse files Browse the repository at this point in the history
  • Loading branch information
morisil committed Dec 2, 2024
1 parent e099f89 commit 0140a95
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 10 deletions.
4 changes: 2 additions & 2 deletions api/xemantic-ai-tool-schema.api
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,8 @@ public final class com/xemantic/ai/tool/schema/StringSchema$Companion {
}

public final class com/xemantic/ai/tool/schema/generator/JsonSchemaGeneratorKt {
public static final fun generateSchema (Lkotlinx/serialization/descriptors/SerialDescriptor;Z)Lcom/xemantic/ai/tool/schema/JsonSchema;
public static synthetic fun generateSchema$default (Lkotlinx/serialization/descriptors/SerialDescriptor;ZILjava/lang/Object;)Lcom/xemantic/ai/tool/schema/JsonSchema;
public static final fun generateSchema (Lkotlinx/serialization/descriptors/SerialDescriptor;ZZ)Lcom/xemantic/ai/tool/schema/JsonSchema;
public static synthetic fun generateSchema$default (Lkotlinx/serialization/descriptors/SerialDescriptor;ZZILjava/lang/Object;)Lcom/xemantic/ai/tool/schema/JsonSchema;
}

public abstract interface annotation class com/xemantic/ai/tool/schema/meta/ContentMediaType : java/lang/annotation/Annotation {
Expand Down
8 changes: 4 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,10 @@ tasks.withType<Test> {
}

powerAssert {
functions = listOf(
"io.kotest.matchers.shouldBe"
)
includedSourceSets = listOf("commonTest", "jvmTest", "jsTest", "nativeTest", "wasmJsTest", "wasmWasiTest")
// power assert temporarily switched off for kotest, since it stopped working with kotlin 2.1
// functions = listOf(
// "io.kotest.matchers.shouldBe"
// )
}

// https://kotlinlang.org/docs/dokka-migration.html#adjust-configuration-options
Expand Down
40 changes: 36 additions & 4 deletions src/commonMain/kotlin/generator/JsonSchemaGenerator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,49 @@ import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlin.collections.set

/**
* Generates a JSON schema for the specified type [T].
*
* This function creates a detailed JSON schema by analyzing the structure and metadata
* of the [SerialDescriptor] extracted from the type [T],
* therefore [T] must be [Serializable].
*
* @param T The type for which to generate the JSON schema.
* @param outputAdditionalPropertiesFalse If `true`, adds `additionalProperties: false` keywords in
* generated [JsonSchema] tree, if a node represents an [ObjectSchema].
* @param suppressDescription If `true`, suppresses the output of the `description` keyword in
* the root of generated [JsonSchema] for the specified type [T], even if it was annotated with the [Description].
* @return A [JsonSchema] representing the structure of type [T].
* @see generateSchema
*/
public inline fun <reified T> jsonSchemaOf(
outputAdditionalPropertiesFalse: Boolean = false
outputAdditionalPropertiesFalse: Boolean = false,
suppressDescription: Boolean = false
): JsonSchema = generateSchema(
descriptor = serializer<T>().descriptor,
outputAdditionalPropertiesFalse = outputAdditionalPropertiesFalse
outputAdditionalPropertiesFalse = outputAdditionalPropertiesFalse,
suppressDescription = suppressDescription
)

/**
* Generates a JSON schema from a given [SerialDescriptor].
*
* This function creates a detailed JSON schema by analyzing the structure and metadata
* of the provided [SerialDescriptor]. It handles nested objects, arrays, and various
* primitive types, incorporating annotations for additional schema properties.
*
* @param descriptor The serial descriptor of the type for which to generate the JSON schema.
* @param outputAdditionalPropertiesFalse If `true`, adds `additionalProperties: false` keywords in
* generated [JsonSchema] tree, if a node represents an [ObjectSchema].
* @param suppressDescription If `true`, suppresses the output of the `description` keyword in
* the root of generated [JsonSchema], even if it was annotated with the [Description].
* @return A [JsonSchema] representing the structure of type described by the [descriptor].
*/
@OptIn(ExperimentalSerializationApi::class)
public fun generateSchema(
descriptor: SerialDescriptor,
outputAdditionalPropertiesFalse: Boolean = false
outputAdditionalPropertiesFalse: Boolean = false,
suppressDescription: Boolean = false
): JsonSchema {

val props = mutableMapOf<String, JsonSchema>()
Expand All @@ -81,7 +113,7 @@ public fun generateSchema(

return ObjectSchema {
title = descriptor.annotations.find<Title>()?.value
description = descriptor.annotations.find<Description>()?.value
description = if (!suppressDescription) descriptor.annotations.find<Description>()?.value else null
properties = props
definitions = if (defs.isNotEmpty()) defs else null
required = req
Expand Down
88 changes: 88 additions & 0 deletions src/commonTest/kotlin/generator/JsonSchemaGeneratorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ class JsonSchemaGeneratorTest {
* so we want to test if we can override it.
*/
@Serializable
@SerialName("foo")
@Description("A container of monetary amounts")
@Suppress("unused") // it is used to generate schema
class Foo(
@Title("Money 1, without property description")
Expand All @@ -347,6 +349,7 @@ class JsonSchemaGeneratorTest {
testJson.encodeToString(schema) shouldEqualJson $$"""
{
"type": "object",
"description": "A container of monetary amounts",
"properties": {
"money1": {
"type": "string",
Expand All @@ -369,4 +372,89 @@ class JsonSchemaGeneratorTest {
"""
}

@Test
fun `should suppress description of the top level object JSON Schema`() {
val schema = jsonSchemaOf<Foo>(
suppressDescription = true
)
testJson.encodeToString(schema) shouldEqualJson $$"""
{
"type": "object",
"properties": {
"money1": {
"type": "string",
"title": "Money 1, without property description",
"description": "A monetary amount",
"pattern": "^-?[0-9]+\\.[0-9]{2}?$"
},
"money2": {
"type": "string",
"title": "Money 2, with property description",
"description": "A monetary amount with property description",
"pattern": "^-?[0-9]+\\.[0-9]{2}?$"
}
},
"required": [
"money1",
"money2"
]
}
"""
}

@Serializable
@Suppress("unused") // it is used to generate schema
class Bar(
val foo: Foo
)

/**
* It seems that `additionalProperties: false` is preferred in the Open AI API documentation.
*/
@Test
fun `should output additionalProperties keyword`() {
val schema = jsonSchemaOf<Bar>(
outputAdditionalPropertiesFalse = true
)
testJson.encodeToString(schema) shouldEqualJson $$"""
{
"type": "object",
"properties": {
"foo": {
"$ref": "#/definitions/foo"
}
},
"required": [
"foo"
],
"definitions": {
"foo": {
"type": "object",
"description": "A container of monetary amounts",
"properties": {
"money1": {
"type": "string",
"title": "Money 1, without property description",
"description": "A monetary amount",
"pattern": "^-?[0-9]+\\.[0-9]{2}?$"
},
"money2": {
"type": "string",
"title": "Money 2, with property description",
"description": "A monetary amount with property description",
"pattern": "^-?[0-9]+\\.[0-9]{2}?$"
}
},
"required": [
"money1",
"money2"
],
"additionalProperties": false
}
},
"additionalProperties": false
}
"""
}

}

0 comments on commit 0140a95

Please sign in to comment.