From 79266be9700daeb894e265b1dae17e1257194769 Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Thu, 7 Nov 2024 17:11:20 -0500 Subject: [PATCH] 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 --- .../ai/converter/BeanOutputConverter.java | 4 ++- .../ai/converter/BeanOutputConverterTest.java | 27 +++++++++++++++++++ .../api/structured-output-converter.adoc | 15 +++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java b/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java index 68b0a3582..0e718c1b9 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/converter/BeanOutputConverter.java @@ -54,6 +54,7 @@ import org.springframework.lang.NonNull; * @author Kirk Lund * @author Josh Long * @author Sebastien Deleuze + * @author Soby Chacko */ public class BeanOutputConverter implements StructuredOutputConverter { @@ -125,7 +126,8 @@ public class BeanOutputConverter implements StructuredOutputConverter { * 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) diff --git a/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java b/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java index 39d1ccb1d..3653d06cf 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/converter/BeanOutputConverterTest.java @@ -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 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() { diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc index 1fc320cd3..79591e656 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/structured-output-converter.adoc @@ -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 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.