Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,41 @@ public open class OpenAIStandardJsonSchemaGenerator : StandardJsonSchemaGenerato
throw UnsupportedOperationException("OpenAI JSON schema doesn't support maps")
}

/*
OpenAI strict-mode validators (notably the GPT-5 family) reject nullable arrays encoded as
a type union `{"type": ["array", "null"], "items": {...}}`. Emit `anyOf` instead, which is
accepted by all current OpenAI strict validators. Non-nullable lists are unchanged.
*/
override fun processList(context: GenerationContext): JsonObject {
if (!context.descriptor.isNullable) {
return super.processList(context)
}

val itemDescriptor = context.descriptor.getElementDescriptor(0)
return buildJsonObject {
put(
JsonSchemaConsts.Keys.ANY_OF,
buildJsonArray {
add(
buildJsonObject {
put(JsonSchemaConsts.Keys.TYPE, JsonSchemaConsts.Types.ARRAY)
put(
JsonSchemaConsts.Keys.ITEMS,
process(context.copy(descriptor = itemDescriptor, currentDescription = null))
)
}
)
add(
buildJsonObject {
put(JsonSchemaConsts.Keys.TYPE, JsonSchemaConsts.Types.NULL)
}
)
}
)
context.currentDescription?.let { put(JsonSchemaConsts.Keys.DESCRIPTION, it) }
}
}

override fun processObject(context: GenerationContext): JsonObject {
val refObject = super.processObject(context).toMutableMap()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,137 @@ class OpenAIStandardJsonSchemaGeneratorTest {
}
}

@Serializable
@SerialName("Tag")
data class Tag(
val name: String
)

@Serializable
@SerialName("NullableListContainer")
data class NullableListContainer(
val name: String,
@property:LLMDescription("Optional list of tags")
val tags: List<String>? = null,
@property:LLMDescription("Optional list of complex tags")
val complexTags: List<Tag>? = null
)

@Test
fun testGenerateOpenAIStandardJsonSchemaForNullableLists() {
val result = fullGenerator.generate(
json,
"NullableListContainer",
serializer<NullableListContainer>(),
emptyMap()
)
val schema = json.encodeToString(result.schema)

val expectedSchema = """
{
"${"$"}id": "NullableListContainer",
"${"$"}defs": {
"Tag": {
"type": "object",
"properties": {
"name": {
"type": "string"
}
},
"required": [
"name"
],
"additionalProperties": false
},
"NullableListContainer": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"tags": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Optional list of tags"
},
"complexTags": {
"anyOf": [
{
"type": "array",
"items": {
"${"$"}ref": "#/${"$"}defs/Tag"
}
},
{
"type": "null"
}
],
"description": "Optional list of complex tags"
}
},
"required": [
"name",
"tags",
"complexTags"
],
"additionalProperties": false
}
},
"type": "object",
"properties": {
"name": {
"type": "string"
},
"tags": {
"anyOf": [
{
"type": "array",
"items": {
"type": "string"
}
},
{
"type": "null"
}
],
"description": "Optional list of tags"
},
"complexTags": {
"anyOf": [
{
"type": "array",
"items": {
"${"$"}ref": "#/${"$"}defs/Tag"
}
},
{
"type": "null"
}
],
"description": "Optional list of complex tags"
}
},
"required": [
"name",
"tags",
"complexTags"
],
"additionalProperties": false
}
""".trimIndent()

assertEquals(expectedSchema, schema)
}

@Test
fun testGenerateOpenAIStandardJsonSchemaWeatherForecast() {
val result = fullGenerator.generate(json, "WeatherForecast", serializer<WeatherForecast>(), emptyMap())
Expand Down
Loading