diff --git a/api/xemantic-ai-tool-schema.api b/api/xemantic-ai-tool-schema.api index 55fc548..3ef999d 100644 --- a/api/xemantic-ai-tool-schema.api +++ b/api/xemantic-ai-tool-schema.api @@ -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 { diff --git a/build.gradle.kts b/build.gradle.kts index 04b6df4..818605c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -178,10 +178,10 @@ tasks.withType { } 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 diff --git a/src/commonMain/kotlin/generator/JsonSchemaGenerator.kt b/src/commonMain/kotlin/generator/JsonSchemaGenerator.kt index d17c612..8f7a8e4 100644 --- a/src/commonMain/kotlin/generator/JsonSchemaGenerator.kt +++ b/src/commonMain/kotlin/generator/JsonSchemaGenerator.kt @@ -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 jsonSchemaOf( - outputAdditionalPropertiesFalse: Boolean = false + outputAdditionalPropertiesFalse: Boolean = false, + suppressDescription: Boolean = false ): JsonSchema = generateSchema( descriptor = serializer().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() @@ -81,7 +113,7 @@ public fun generateSchema( return ObjectSchema { title = descriptor.annotations.find()?.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 diff --git a/src/commonTest/kotlin/generator/JsonSchemaGeneratorTest.kt b/src/commonTest/kotlin/generator/JsonSchemaGeneratorTest.kt index 10ffa47..f8e83c3 100644 --- a/src/commonTest/kotlin/generator/JsonSchemaGeneratorTest.kt +++ b/src/commonTest/kotlin/generator/JsonSchemaGeneratorTest.kt @@ -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") @@ -347,6 +349,7 @@ class JsonSchemaGeneratorTest { testJson.encodeToString(schema) shouldEqualJson $$""" { "type": "object", + "description": "A container of monetary amounts", "properties": { "money1": { "type": "string", @@ -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 + } + """ + } + }