From 7be9e37d96def3e13e0e20fced5ba77cc7b9fdc1 Mon Sep 17 00:00:00 2001 From: D Gardner Date: Tue, 24 Jun 2025 18:45:46 +0100 Subject: [PATCH] empty-properties: improved docs and error message. --- README.md | 72 ++++++++++++++----- .../com/openai/core/JsonSchemaValidator.kt | 8 ++- .../com/openai/core/StructuredOutputsTest.kt | 14 ++-- .../StructuredResponseOutputItemTest.kt | 3 +- 4 files changed, 72 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 3933095d2..e10dd1db2 100644 --- a/README.md +++ b/README.md @@ -363,7 +363,8 @@ response will then be converted automatically to an instance of that Java class. example of the use of Structured Outputs with arbitrary Java classes can be seen in [`StructuredOutputsExample`](openai-java-example/src/main/java/com/openai/example/StructuredOutputsExample.java). -Java classes can contain fields declared to be instances of other classes and can use collections: +Java classes can contain fields declared to be instances of other classes and can use collections +(see [Defining JSON schema properties](#defining-json-schema-properties) for more details): ```java class Person { @@ -506,12 +507,38 @@ the latter when `ResponseCreateParams.Builder.text(Class)` is called. For a full example of the usage of _Structured Outputs_ with the Responses API, see [`ResponsesStructuredOutputsExample`](openai-java-example/src/main/java/com/openai/example/ResponsesStructuredOutputsExample.java). +### Defining JSON schema properties + +When a JSON schema is derived from your Java classes, all properties represented by `public` fields +or `public` getter methods are included in the schema by default. Non-`public` fields and getter +methods are _not_ included by default. You can exclude `public`, or include non-`public` fields or +getter methods, by using the `@JsonIgnore` or `@JsonProperty` annotations respectively (see +[Annotating classes and JSON schemas](#annotating-classes-and-json-schemas) for details). + +If you do not want to define `public` fields, you can define `private` fields and corresponding +`public` getter methods. For example, a `private` field `myValue` with a `public` getter method +`getMyValue()` will result in a `"myValue"` property being included in the JSON schema. If you +prefer not to use the conventional Java "get" prefix for the name of the getter method, then you +_must_ annotate the getter method with the `@JsonProperty` annotation and the full method name will +be used as the property name. You do not have to define any corresponding setter methods if you do +not need them. + +Each of your classes _must_ define at least one property to be included in the JSON schema. A +validation error will occur if any class contains no fields or getter methods from which schema +properties can be derived. This may occur if, for example: + +- There are no fields or getter methods in the class. +- All fields and getter methods are `public`, but all are annotated with `@JsonIgnore`. +- All fields and getter methods are non-`public`, but none are annotated with `@JsonProperty`. +- A field or getter method is declared with a `Map` type. A `Map` is treated like a separate class + with no named properties, so it will result in an empty `"properties"` field in the JSON schema. + ### Annotating classes and JSON schemas You can use annotations to add further information to the JSON schema derived from your Java -classes, or to exclude individual fields from the schema. Details from annotations captured in the -JSON schema may be used by the AI model to improve its response. The SDK supports the use of -[Jackson Databind](https://github.com/FasterXML/jackson-databind) annotations. +classes, or to control which fields or getter methods will be included in the schema. Details from +annotations captured in the JSON schema may be used by the AI model to improve its response. The SDK +supports the use of [Jackson Databind](https://github.com/FasterXML/jackson-databind) annotations. ```java import com.fasterxml.jackson.annotation.JsonClassDescription; @@ -541,8 +568,12 @@ class BookList { ``` - Use `@JsonClassDescription` to add a detailed description to a class. -- Use `@JsonPropertyDescription` to add a detailed description to a field of a class. -- Use `@JsonIgnore` to omit a field of a class from the generated JSON schema. +- Use `@JsonPropertyDescription` to add a detailed description to a field or getter method of a + class. +- Use `@JsonIgnore` to exclude a `public` field or getter method of a class from the generated JSON + schema. +- Use `@JsonProperty` to include a non-`public` field or getter method of a class in the generated + JSON schema. If you use `@JsonProperty(required = false)`, the `false` value will be ignored. OpenAI JSON schemas must mark all properties as _required_, so the schema generated from your Java classes will respect @@ -577,9 +608,11 @@ _Function Calling_ with Java classes to define function parameters can be seen i [`FunctionCallingExample`](openai-java-example/src/main/java/com/openai/example/FunctionCallingExample.java). Like for [Structured Outputs](#structured-outputs-with-json-schemas), Java classes can contain -fields declared to be instances of other classes and can use collections. Optionally, annotations -can be used to set the descriptions of the function (class) and its parameters (fields) to assist -the AI model in understanding the purpose of the function and the possible values of its parameters. +fields declared to be instances of other classes and can use collections (see +[Defining JSON schema properties](#defining-json-schema-properties) for more details). Optionally, +annotations can be used to set the descriptions of the function (class) and its parameters (fields) +to assist the AI model in understanding the purpose of the function and the possible values of its +parameters. ```java import com.fasterxml.jackson.annotation.JsonClassDescription; @@ -724,24 +757,31 @@ validation and under what circumstances you might want to disable it. ### Annotating function classes You can use annotations to add further information about functions to the JSON schemas that are -derived from your function classes, or to exclude individual fields from the parameters to the -function. Details from annotations captured in the JSON schema may be used by the AI model to -improve its response. The SDK supports the use of +derived from your function classes, or to control which fields or getter methods will be used as +parameters to the function. Details from annotations captured in the JSON schema may be used by the +AI model to improve its response. The SDK supports the use of [Jackson Databind](https://github.com/FasterXML/jackson-databind) annotations. - Use `@JsonClassDescription` to add a description to a function class detailing when and how to use that function. - Use `@JsonTypeName` to set the function name to something other than the simple name of the class, which is used by default. -- Use `@JsonPropertyDescription` to add a detailed description to function parameter (a field of - a function class). -- Use `@JsonIgnore` to omit a field of a class from the generated JSON schema for a function's - parameters. +- Use `@JsonPropertyDescription` to add a detailed description to function parameter (a field or + getter method of a function class). +- Use `@JsonIgnore` to exclude a `public` field or getter method of a class from the generated JSON + schema for a function's parameters. +- Use `@JsonProperty` to include a non-`public` field or getter method of a class in the generated + JSON schema for a function's parameters. OpenAI provides some [Best practices for defining functions](https://platform.openai.com/docs/guides/function-calling#best-practices-for-defining-functions) that may help you to understand how to use the above annotations effectively for your functions. +See also [Defining JSON schema properties](#defining-json-schema-properties) for more details on how +to use fields and getter methods and combine access modifiers and annotations to define the +parameters of your functions. The same rules apply to function classes and to the structured output +classes described in that section. + ## File uploads The SDK defines methods that accept files. diff --git a/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt b/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt index f3d74364f..ba30f2294 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt @@ -19,6 +19,9 @@ import com.openai.core.JsonSchemaValidator.Companion.UNRESTRICTED_ENUM_VALUES_LI internal class JsonSchemaValidator private constructor() { companion object { + private const val NO_PROPERTIES_DOC = + "https://github.com/openai/openai-java/blob/main/README.md#defining-json-schema-properties" + // The names of the supported schema keywords. All other keywords will be rejected. private const val SCHEMA = "\$schema" private const val ID = "\$id" @@ -409,7 +412,10 @@ internal class JsonSchemaValidator private constructor() { verify( properties != null && properties.isObject && !properties.isEmpty, path, - { "'$PROPS' field is missing, empty or not an object." }, + { + "'$PROPS' field is missing, empty or not an object. " + + "At least one named property must be defined. See: $NO_PROPERTIES_DOC" + }, ) { return } diff --git a/openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTest.kt index 970b3dbe0..cf5dedd29 100644 --- a/openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/core/StructuredOutputsTest.kt @@ -84,7 +84,7 @@ internal class StructuredOutputsTest { // AI. It can only reply with values to _named_ properties, so there must be at least one. assertThat(validator.errors()).hasSize(1) assertThat(validator.errors()[0]) - .isEqualTo("#: 'properties' field is missing, empty or not an object.") + .startsWith("#: 'properties' field is missing, empty or not an object.") } @Test @@ -98,15 +98,15 @@ internal class StructuredOutputsTest { // no named `"properties"` and no `"required"` array. Only the first problem is reported. assertThat(validator.errors()).hasSize(1) assertThat(validator.errors()[0]) - .isEqualTo("#/properties/m: 'properties' field is missing, empty or not an object.") + .startsWith("#/properties/m: 'properties' field is missing, empty or not an object.") // Do this check of `toString()` once for a validation failure, but do not repeat it in // other tests. assertThat(validator.toString()) - .isEqualTo( + .startsWith( "JsonSchemaValidator{isValidationComplete=true, totalStringLength=1, " + "totalObjectProperties=1, totalEnumValues=0, errors=[" + - "#/properties/m: 'properties' field is missing, empty or not an object.]}" + "#/properties/m: 'properties' field is missing, empty or not an object." ) } @@ -700,7 +700,7 @@ internal class StructuredOutputsTest { // be allowed and the AI model will have nothing it can populate. assertThat(validator.errors()).hasSize(1) assertThat(validator.errors()[0]) - .isEqualTo("#: 'properties' field is missing, empty or not an object.") + .startsWith("#: 'properties' field is missing, empty or not an object.") } @Test @@ -721,7 +721,7 @@ internal class StructuredOutputsTest { assertThat(validator.errors()).hasSize(1) assertThat(validator.errors()[0]) - .isEqualTo("#: 'properties' field is missing, empty or not an object.") + .startsWith("#: 'properties' field is missing, empty or not an object.") } @Test @@ -742,7 +742,7 @@ internal class StructuredOutputsTest { assertThat(validator.errors()).hasSize(1) assertThat(validator.errors()[0]) - .isEqualTo("#: 'properties' field is missing, empty or not an object.") + .startsWith("#: 'properties' field is missing, empty or not an object.") } @Test diff --git a/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseOutputItemTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseOutputItemTest.kt index 30e50c435..fe8df7128 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseOutputItemTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/responses/StructuredResponseOutputItemTest.kt @@ -60,7 +60,8 @@ internal class StructuredResponseOutputItemTest { ResponseCodeInterpreterToolCall.builder() .id(STRING) .code(STRING) - .addLogsResult(STRING) + .containerId(STRING) + .outputs(listOf()) .status(ResponseCodeInterpreterToolCall.Status.COMPLETED) .build() private val IMAGE_GENERATION_CALL =