GH-1335: Add JSON schema property order support

Fixes: #1335

https://github.com/spring-projects/spring-ai/issues/1335

Add support for maintaining JSON property order in generated schemas using
@JsonPropertyOrder. Users can now control the order of properties in their
JSON schemas by annotating their classes/records with @JsonPropertyOrder.

- Add JacksonOption.RESPECT_JSONPROPERTY_ORDER to BeanOutputConverter
- Add test to verify schema property ordering
- Update documentation with property ordering example
This commit is contained in:
Soby Chacko
2024-11-07 17:11:20 -05:00
committed by Ilayaperumal Gopinathan
parent 9fa89a2fd6
commit 79266be970
3 changed files with 45 additions and 1 deletions

View File

@@ -54,6 +54,7 @@ import org.springframework.lang.NonNull;
* @author Kirk Lund
* @author Josh Long
* @author Sebastien Deleuze
* @author Soby Chacko
*/
public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
@@ -125,7 +126,8 @@ public class BeanOutputConverter<T> implements StructuredOutputConverter<T> {
* Generates the JSON schema for the target type.
*/
private void generateSchema() {
JacksonModule jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED);
JacksonModule jacksonModule = new JacksonModule(JacksonOption.RESPECT_JSONPROPERTY_REQUIRED,
JacksonOption.RESPECT_JSONPROPERTY_ORDER);
SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(
com.github.victools.jsonschema.generator.SchemaVersion.DRAFT_2020_12,
com.github.victools.jsonschema.generator.OptionPreset.PLAIN_JSON)

View File

@@ -17,11 +17,14 @@
package org.springframework.ai.converter;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -37,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Sebastian Ullrich
* @author Kirk Lund
* @author Christian Tzolov
* @author Soby Chacko
*/
@ExtendWith(MockitoExtension.class)
class BeanOutputConverterTest {
@@ -112,6 +116,15 @@ class BeanOutputConverterTest {
}
@JsonPropertyOrder({ "string_property", "foo_property", "bar_property" })
record TestClassWithJsonPropertyOrder(
@JsonProperty("string_property") @JsonPropertyDescription("string_property_description") String someString,
@JsonProperty(required = true, value = "foo_property") String foo,
@JsonProperty(required = true, value = "bar_property") String bar) {
}
@Nested
class ConverterTest {
@@ -155,6 +168,20 @@ class BeanOutputConverterTest {
assertThat(testClass.getSomeString()).isEqualTo("some value");
}
@Test
void verifySchemaPropertyOrder() throws Exception {
var converter = new BeanOutputConverter<>(TestClassWithJsonPropertyOrder.class);
String jsonSchema = converter.getJsonSchema();
ObjectMapper mapper = new ObjectMapper();
JsonNode schemaNode = mapper.readTree(jsonSchema);
List<String> actualOrder = new ArrayList<>();
schemaNode.get("properties").fieldNames().forEachRemaining(actualOrder::add);
assertThat(actualOrder).containsExactly("string_property", "foo_property", "bar_property");
}
@Test
void convertTypeReferenceWithJsonAnnotations() {
var converter = new BeanOutputConverter<>(new ParameterizedTypeReference<TestClassWithJsonAnnotations>() {

View File

@@ -139,6 +139,21 @@ Generation generation = chatModel.call(
ActorsFilms actorsFilms = this.beanOutputConverter.convert(this.generation.getOutput().getContent());
----
=== Property Ordering in Generated Schema
The `BeanOutputConverter` supports custom property ordering in the generated JSON schema through the `@JsonPropertyOrder` annotation.
This annotation allows you to specify the exact sequence in which properties should appear in the schema, regardless of their declaration order in the class or record.
For example, to ensure specific ordering of properties in the `ActorsFilms` record:
[source,java]
----
@JsonPropertyOrder({"actor", "movies"})
record ActorsFilms(String actor, List<String> movies) {}
----
This annotation works with both records and regular Java classes.
==== Generic Bean Types
Use the `ParameterizedTypeReference` constructor to specify a more complex target class structure.