Skip to content

Commit 8c0d60a

Browse files
committed
structured-outputs: changes from code review
1 parent ef0e041 commit 8c0d60a

File tree

11 files changed

+170
-78
lines changed

11 files changed

+170
-78
lines changed

README.md

+35-13
Original file line numberDiff line numberDiff line change
@@ -343,21 +343,23 @@ is a feature that ensures that the model will always generate responses that adh
343343
A JSON schema can be defined by creating a
344344
[`ResponseFormatJsonSchema`](openai-java-core/src/main/kotlin/com/openai/models/ResponseFormatJsonSchema.kt)
345345
and setting it on the input parameters. However, for greater convenience, a JSON schema can instead
346-
be derived automatically from the structure of an arbitrary Java class. The response will then
347-
automatically convert the generated JSON content to an instance of that Java class.
346+
be derived automatically from the structure of an arbitrary Java class. The JSON content from the
347+
response will then be converted automatically to an instance of that Java class. A full, working
348+
example of the use of Structured Outputs with arbitrary Java classes can be seen in
349+
[`StructuredOutputsClassExample`](openai-java-example/src/main/java/com/openai/example/StructuredOutputsClassExample.java).
348350

349351
Java classes can contain fields declared to be instances of other classes and can use collections:
350352

351353
```java
352354
class Person {
353355
public String name;
354-
public int yearOfBirth;
356+
public int birthYear;
355357
}
356358

357359
class Book {
358360
public String title;
359361
public Person author;
360-
public int yearPublished;
362+
public int publicationYear;
361363
}
362364

363365
class BookList {
@@ -375,7 +377,7 @@ import com.openai.models.chat.completions.ChatCompletionCreateParams;
375377
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
376378

377379
StructuredChatCompletionCreateParams<BookList> params = ChatCompletionCreateParams.builder()
378-
.addUserMessage("List six famous nineteenth century novels.")
380+
.addUserMessage("List some famous late twentieth century novels.")
379381
.model(ChatModel.GPT_4_1)
380382
.responseFormat(BookList.class)
381383
.build();
@@ -403,11 +405,25 @@ import java.util.Optional;
403405
class Book {
404406
public String title;
405407
public Person author;
406-
public int yearPublished;
408+
public int publicationYear;
407409
public Optional<String> isbn;
408410
}
409411
```
410412

413+
Generic type information for fields is retained in the class's metadata, but _generic type erasure_
414+
applies in other scopes. While, for example, a JSON schema defining an array of strings can be
415+
derived from the `BoolList.books` field with type `List<String>`, a valid JSON schema cannot be
416+
derived from a local variable of that same type, so the following will _not_ work:
417+
418+
```java
419+
List<String> books = new ArrayList<>();
420+
421+
StructuredChatCompletionCreateParams<BookList> params = ChatCompletionCreateParams.builder()
422+
.responseFormat(books.class)
423+
// ...
424+
.build();
425+
```
426+
411427
If an error occurs while converting a JSON response to an instance of a Java class, the error
412428
message will include the JSON response to assist in diagnosis. For instance, if the response is
413429
truncated, the JSON data will be incomplete and cannot be converted to a class instance. If your
@@ -435,20 +451,23 @@ well.
435451
schema in the request.
436452
- **Version Compatibility**: There may be instances where local validation fails while remote
437453
validation succeeds. This can occur if the SDK version is outdated compared to the restrictions
438-
enforced by the remote model.
454+
enforced by the remote AI model.
439455
- **Disabling Local Validation**: If you encounter compatibility issues and wish to bypass local
440-
validation, you can disable it by passing `false` to the `responseFormat(Class<T>, boolean)` method
441-
when building the parameters. (The default value for this parameter is `true`.)
456+
validation, you can disable it by passing
457+
[`JsonSchemaLocalValidation.NO`](openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaLocalValidation.kt)
458+
to the `responseFormat(Class<T>, JsonSchemaLocalValidation)` method when building the parameters.
459+
(The default value for this parameter is `JsonSchemaLocalValidation.YES`.)
442460

443461
```java
462+
import com.openai.core.JsonSchemaLocalValidation;
444463
import com.openai.models.ChatModel;
445464
import com.openai.models.chat.completions.ChatCompletionCreateParams;
446465
import com.openai.models.chat.completions.StructuredChatCompletionCreateParams;
447466

448467
StructuredChatCompletionCreateParams<BookList> params = ChatCompletionCreateParams.builder()
449-
.addUserMessage("List six famous nineteenth century novels.")
468+
.addUserMessage("List some famous late twentieth century novels.")
450469
.model(ChatModel.GPT_4_1)
451-
.responseFormat(BookList.class, false) // Disable local validation.
470+
.responseFormat(BookList.class, JsonSchemaLocalValidation.NO)
452471
.build();
453472
```
454473

@@ -470,14 +489,17 @@ import com.fasterxml.jackson.annotation.JsonPropertyDescription;
470489
class Person {
471490
@JsonPropertyDescription("The first name and surname of the person")
472491
public String name;
473-
public int yearOfBirth;
492+
public int birthYear;
493+
@JsonPropertyDescription("The year the person died, or 'present' if the person is living.")
494+
public String deathYear;
474495
}
475496

476497
@JsonClassDescription("The details of one published book")
477498
class Book {
478499
public String title;
479500
public Person author;
480-
public int yearPublished;
501+
@JsonPropertyDescription("The year in which the book was first published.")
502+
public int publicationYear;
481503
@JsonIgnore public String genre;
482504
}
483505

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.openai.core
2+
3+
/**
4+
* Options for local validation of JSON schemas derived from arbitrary classes before a request is
5+
* executed.
6+
*/
7+
enum class JsonSchemaLocalValidation {
8+
/**
9+
* Validate the JSON schema locally before the request is executed. The remote AI model will
10+
* also validate the JSON schema.
11+
*/
12+
YES,
13+
14+
/**
15+
* Do not validate the JSON schema locally before the request is executed. The remote AI model
16+
* will always validate the JSON schema.
17+
*/
18+
NO,
19+
}

openai-java-core/src/main/kotlin/com/openai/core/JsonSchemaValidator.kt

+1-3
Original file line numberDiff line numberDiff line change
@@ -201,9 +201,7 @@ internal class JsonSchemaValidator private constructor() {
201201
* each new schema.
202202
*/
203203
fun validate(rootSchema: JsonNode): JsonSchemaValidator {
204-
if (isValidationComplete) {
205-
throw IllegalStateException("Validation already complete.")
206-
}
204+
check(!isValidationComplete) { "Validation already complete." }
207205
isValidationComplete = true
208206

209207
validateSchema(rootSchema, ROOT_PATH, ROOT_DEPTH)

openai-java-core/src/main/kotlin/com/openai/core/StructuredOutputs.kt

+9-8
Original file line numberDiff line numberDiff line change
@@ -23,33 +23,33 @@ private val MAPPER =
2323
.addModule(JavaTimeModule())
2424
.build()
2525

26+
@JvmSynthetic
2627
internal fun <T> fromClass(
2728
type: Class<T>,
28-
localValidation: Boolean = true,
29+
localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
2930
): ResponseFormatJsonSchema {
3031
val schema = extractSchema(type)
3132

32-
if (localValidation) {
33+
if (localValidation == JsonSchemaLocalValidation.YES) {
3334
val validator = JsonSchemaValidator.create().validate(schema)
3435

35-
if (!validator.isValid()) {
36-
throw IllegalArgumentException(
37-
"Local validation failed for JSON schema derived from $type:\n" +
38-
validator.errors().joinToString("\n") { " - $it" }
39-
)
36+
require(validator.isValid()) {
37+
"Local validation failed for JSON schema derived from $type:\n" +
38+
validator.errors().joinToString("\n") { " - $it" }
4039
}
4140
}
4241

4342
return ResponseFormatJsonSchema.builder()
4443
.jsonSchema(
4544
ResponseFormatJsonSchema.JsonSchema.builder()
4645
.name("json-schema-from-${type.simpleName}")
47-
.schema(JsonValue.from(schema))
46+
.schema(JsonValue.fromJsonNode(schema))
4847
.build()
4948
)
5049
.build()
5150
}
5251

52+
@JvmSynthetic
5353
internal fun <T> extractSchema(type: Class<T>): JsonNode {
5454
// Validation is not performed by this function, as it allows extraction of the schema and
5555
// validation of the schema to be controlled more easily when unit testing, as no exceptions
@@ -76,6 +76,7 @@ internal fun <T> extractSchema(type: Class<T>): JsonNode {
7676
return SchemaGenerator(configBuilder.build()).generateSchema(type)
7777
}
7878

79+
@JvmSynthetic
7980
internal fun <T> fromJson(json: String, type: Class<T>): T =
8081
try {
8182
MAPPER.readValue(json, type)

openai-java-core/src/main/kotlin/com/openai/models/chat/completions/ChatCompletionCreateParams.kt

+11-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import com.openai.core.Enum
1919
import com.openai.core.ExcludeMissing
2020
import com.openai.core.JsonField
2121
import com.openai.core.JsonMissing
22+
import com.openai.core.JsonSchemaLocalValidation
2223
import com.openai.core.JsonValue
2324
import com.openai.core.Params
2425
import com.openai.core.allMaxBy
@@ -1304,15 +1305,20 @@ private constructor(
13041305
*
13051306
* @param responseFormat A class from which a JSON schema will be derived to define the
13061307
* response format.
1307-
* @param localValidation `true` (the default) to validate the JSON schema locally when it
1308-
* is generated by this method to confirm that it adheres to the requirements and
1309-
* restrictions on JSON schemas imposed by the OpenAI specification; or `false` to disable
1310-
* local validation. See the SDK documentation for more details.
1308+
* @param localValidation [com.openai.core.JsonSchemaLocalValidation.YES] (the default) to
1309+
* validate the JSON schema locally when it is generated by this method to confirm that it
1310+
* adheres to the requirements and restrictions on JSON schemas imposed by the OpenAI
1311+
* specification; or [com.openai.core.JsonSchemaLocalValidation.NO] to skip local
1312+
* validation and rely only on remote validation. See the SDK documentation for more
1313+
* details.
13111314
* @throws IllegalArgumentException If local validation is enabled, but it fails because a
13121315
* valid JSON schema cannot be derived from the given class.
13131316
*/
13141317
@JvmOverloads
1315-
fun <T : Any> responseFormat(responseFormat: Class<T>, localValidation: Boolean = true) =
1318+
fun <T : Any> responseFormat(
1319+
responseFormat: Class<T>,
1320+
localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
1321+
) =
13161322
StructuredChatCompletionCreateParams.builder<T>()
13171323
.wrap(responseFormat, this, localValidation)
13181324

openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletion.kt

+11-4
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ import com.openai.models.completions.CompletionUsage
1010
import java.util.Objects
1111
import java.util.Optional
1212

13+
/**
14+
* A wrapper for [ChatCompletion] that provides type-safe access to the [choices] when using the
15+
* _Structured Outputs_ feature to deserialize a JSON response to an instance of an arbitrary class.
16+
* See the SDK documentation for more details on _Structured Outputs_.
17+
*
18+
* @param T The type of the class to which the JSON data in the response will be deserialized.
19+
*/
1320
class StructuredChatCompletion<T : Any>(
14-
val responseFormat: Class<T>,
15-
val chatCompletion: ChatCompletion,
21+
@get:JvmName("responseFormat") val responseFormat: Class<T>,
22+
@get:JvmName("chatCompletion") val chatCompletion: ChatCompletion,
1623
) {
1724
/** @see ChatCompletion.id */
1825
fun id(): String = chatCompletion.id()
@@ -68,8 +75,8 @@ class StructuredChatCompletion<T : Any>(
6875

6976
class Choice<T : Any>
7077
internal constructor(
71-
internal val responseFormat: Class<T>,
72-
internal val choice: ChatCompletion.Choice,
78+
@get:JvmName("responseFormat") val responseFormat: Class<T>,
79+
@get:JvmName("choice") val choice: ChatCompletion.Choice,
7380
) {
7481
/** @see ChatCompletion.Choice.finishReason */
7582
fun finishReason(): FinishReason = choice.finishReason()

openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletionCreateParams.kt

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.openai.models.chat.completions
22

33
import com.openai.core.JsonField
4+
import com.openai.core.JsonSchemaLocalValidation
45
import com.openai.core.JsonValue
56
import com.openai.core.checkRequired
67
import com.openai.core.fromClass
@@ -11,9 +12,18 @@ import com.openai.models.ReasoningEffort
1112
import java.util.Objects
1213
import java.util.Optional
1314

15+
/**
16+
* A wrapper for [ChatCompletionCreateParams] that provides a type-safe [Builder] that can record
17+
* the type of the [responseFormat] used to derive a JSON schema from an arbitrary class when using
18+
* the _Structured Outputs_ feature. When a JSON response is received, it is deserialized to am
19+
* instance of that type. See the SDK documentation for more details on _Structured Outputs_.
20+
*
21+
* @param T The type of the class that will be used to derive the JSON schema in the request and to
22+
* which the JSON response will be deserialized.
23+
*/
1424
class StructuredChatCompletionCreateParams<T : Any>
1525
internal constructor(
16-
val responseFormat: Class<T>,
26+
@get:JvmName("responseFormat") val responseFormat: Class<T>,
1727
/**
1828
* The raw, underlying chat completion create parameters wrapped by this structured instance of
1929
* the parameters.
@@ -33,7 +43,7 @@ internal constructor(
3343
internal fun wrap(
3444
responseFormat: Class<T>,
3545
paramsBuilder: ChatCompletionCreateParams.Builder,
36-
localValidation: Boolean,
46+
localValidation: JsonSchemaLocalValidation,
3747
) = apply {
3848
this.responseFormat = responseFormat
3949
this.paramsBuilder = paramsBuilder
@@ -396,7 +406,10 @@ internal constructor(
396406
* @see ChatCompletionCreateParams.Builder.responseFormat
397407
*/
398408
@JvmOverloads
399-
fun responseFormat(responseFormat: Class<T>, localValidation: Boolean = true) = apply {
409+
fun responseFormat(
410+
responseFormat: Class<T>,
411+
localValidation: JsonSchemaLocalValidation = JsonSchemaLocalValidation.YES,
412+
) = apply {
400413
this.responseFormat = responseFormat
401414
paramsBuilder.responseFormat(fromClass(responseFormat, localValidation))
402415
}

openai-java-core/src/main/kotlin/com/openai/models/chat/completions/StructuredChatCompletionMessage.kt

+10-2
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ import com.openai.models.chat.completions.ChatCompletionMessage.FunctionCall
77
import java.util.Objects
88
import java.util.Optional
99

10+
/**
11+
* A wrapper for [ChatCompletionMessage] that provides type-safe access to the [content] when using
12+
* the _Structured Outputs_ feature to deserialize a JSON response to an instance of an arbitrary
13+
* class. See the SDK documentation for more details on _Structured Outputs_.
14+
*
15+
* @param T The type of the class to which the JSON data in the content will be deserialized when
16+
* [content] is called.
17+
*/
1018
class StructuredChatCompletionMessage<T : Any>
1119
internal constructor(
12-
val responseFormat: Class<T>,
13-
val chatCompletionMessage: ChatCompletionMessage,
20+
@get:JvmName("responseFormat") val responseFormat: Class<T>,
21+
@get:JvmName("chatCompletionMessage") val chatCompletionMessage: ChatCompletionMessage,
1422
) {
1523

1624
private val content: JsonField<T> by lazy {

openai-java-core/src/main/kotlin/com/openai/services/blocking/chat/ChatCompletionService.kt

+7-5
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,14 @@ interface ChatCompletionService {
5858
/** @see create */
5959
fun <T : Any> create(
6060
params: StructuredChatCompletionCreateParams<T>
61+
): StructuredChatCompletion<T> = create(params, RequestOptions.none())
62+
63+
/** @see create */
64+
fun <T : Any> create(
65+
params: StructuredChatCompletionCreateParams<T>,
66+
requestOptions: RequestOptions = RequestOptions.none(),
6167
): StructuredChatCompletion<T> =
62-
StructuredChatCompletion<T>(
63-
params.responseFormat,
64-
// Normal, non-generic create method call via `ChatCompletionCreateParams`.
65-
create(params.rawParams),
66-
)
68+
StructuredChatCompletion<T>(params.responseFormat, create(params.rawParams, requestOptions))
6769

6870
/**
6971
* **Starting a new project?** We recommend trying

0 commit comments

Comments
 (0)