diff --git a/src/main/java/com/google/genai/Transformers.java b/src/main/java/com/google/genai/Transformers.java index 75deb53a806..de74020949c 100644 --- a/src/main/java/com/google/genai/Transformers.java +++ b/src/main/java/com/google/genai/Transformers.java @@ -43,6 +43,8 @@ import com.google.genai.types.VoiceConfig; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.List; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -221,14 +223,58 @@ public static Content tContent(Object content) { /** Transforms an object to a Schema for the API. */ public static Schema tSchema(Object origin) { + Schema schema = null; if (origin == null) { return null; } else if (origin instanceof Schema) { - return (Schema) origin; + schema = (Schema) origin; } else if (origin instanceof JsonNode) { - return JsonSerializable.fromJsonNode((JsonNode) origin, Schema.class); + schema = JsonSerializable.fromJsonNode((JsonNode) origin, Schema.class); + } else { + throw new IllegalArgumentException("Unsupported schema type: " + origin.getClass()); + } + return populatePropertyOrdering(schema); + } + + private static Schema populatePropertyOrdering(Schema schema) { + if (schema == null) { + return null; } - throw new IllegalArgumentException("Unsupported schema type: " + origin.getClass()); + Schema.Builder builder = schema.toBuilder(); + boolean modified = false; + + if (schema.properties().isPresent() && !schema.properties().get().isEmpty()) { + Map updatedProperties = new LinkedHashMap<>(); + boolean childModified = false; + for (Map.Entry entry : schema.properties().get().entrySet()) { + Schema originalChild = entry.getValue(); + Schema populatedChild = populatePropertyOrdering(originalChild); + if (populatedChild != originalChild) { + childModified = true; + } + updatedProperties.put(entry.getKey(), populatedChild); + } + if (childModified) { + builder.properties(updatedProperties); + modified = true; + } + + if (!schema.propertyOrdering().isPresent() && schema.properties().get().size() > 1) { + builder.propertyOrdering(new ArrayList<>(schema.properties().get().keySet())); + modified = true; + } + } + + if (schema.items().isPresent()) { + Schema originalItems = schema.items().get(); + Schema populatedItems = populatePropertyOrdering(originalItems); + if (populatedItems != originalItems) { + builder.items(populatedItems); + modified = true; + } + } + + return modified ? builder.build() : schema; } public static SpeechConfig tSpeechConfig(Object speechConfig) { diff --git a/src/main/java/com/google/genai/types/Schema.java b/src/main/java/com/google/genai/types/Schema.java index 00942c36459..5f65ac5dbe5 100644 --- a/src/main/java/com/google/genai/types/Schema.java +++ b/src/main/java/com/google/genai/types/Schema.java @@ -26,10 +26,8 @@ import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.genai.JsonSerializable; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; + +import java.util.*; /** * Schema is used to define the format of input/output data. @@ -698,7 +696,18 @@ public Builder type(String type) { return type(new Type(type)); } - public abstract Schema build(); + abstract Schema autoBuild(); + + abstract Optional> properties(); + + abstract Optional> propertyOrdering(); + + public Schema build() { + if (!propertyOrdering().isPresent() && properties().isPresent() && properties().get().size() > 1) { + propertyOrdering(new ArrayList<>(properties().get().keySet())); + } + return autoBuild(); + } } /** Deserializes a JSON string to a Schema object. */ diff --git a/src/test/java/com/google/genai/TransformersTest.java b/src/test/java/com/google/genai/TransformersTest.java index cf13d6c6637..9e37c3ae3d3 100644 --- a/src/test/java/com/google/genai/TransformersTest.java +++ b/src/test/java/com/google/genai/TransformersTest.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.genai.types.File; import com.google.genai.types.FunctionDeclaration; @@ -126,6 +127,43 @@ public void testTSchema_Required_success() { assertEquals(schema, transformedSchema); } + @Test + public void testTSchema_PropertyOrdering_autoPopulated() { + Schema schema = + Schema.builder() + .type("OBJECT") + .properties( + ImmutableMap.of( + "name", Schema.builder().type("STRING").build(), + "age", Schema.builder().type("INTEGER").build())) + .build(); + + Schema transformedSchema = Transformers.tSchema(schema); + assertTrue(transformedSchema.propertyOrdering().isPresent()); + assertEquals( + ImmutableList.of("name", "age"), + transformedSchema.propertyOrdering().get()); + } + + @Test + public void testTSchema_PropertyOrdering_preservedIfExplicit() { + Schema schema = + Schema.builder() + .type("OBJECT") + .properties( + ImmutableMap.of( + "name", Schema.builder().type("STRING").build(), + "age", Schema.builder().type("INTEGER").build())) + .propertyOrdering("age", "name") + .build(); + + Schema transformedSchema = Transformers.tSchema(schema); + assertTrue(transformedSchema.propertyOrdering().isPresent()); + assertEquals( + ImmutableList.of("age", "name"), + transformedSchema.propertyOrdering().get()); + } + @Test public void testTTool_noFunctions_success() { Tool tool =