Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -935,7 +935,7 @@ private Function() {
public Function(String description, String name, Map<String, Object> parameters, Boolean strict) {
this.description = description;
this.name = name;
this.parameters = parameters;
this.parameters = ensureValidParametersSchema(parameters);
this.strict = strict;
}

Expand All @@ -946,7 +946,39 @@ public Function(String description, String name, Map<String, Object> parameters,
* @param jsonSchema tool function schema as json.
*/
public Function(String description, String name, String jsonSchema) {
this(description, name, ModelOptionsUtils.jsonToMap(jsonSchema), null);
this.description = description;
this.name = name;
this.parameters = ensureValidParametersSchema(
jsonSchema != null ? ModelOptionsUtils.jsonToMap(jsonSchema) : null);
this.strict = null;
}

/**
* Ensures that the parameters schema is valid for OpenAI API. OpenAI requires
* that the parameters object must have a "properties" field, even if it's
* empty.
* @param parameters the parameters map from JSON schema
* @return a valid parameters map with required fields
*/
private static Map<String, Object> ensureValidParametersSchema(Map<String, Object> parameters) {
if (parameters == null) {
parameters = new java.util.HashMap<>();
parameters.put("type", "object");
parameters.put("properties", new java.util.HashMap<>());
return parameters;
}

// Ensure "type" field exists
if (!parameters.containsKey("type")) {
parameters.put("type", "object");
}

// Ensure "properties" field exists for object types
if ("object".equals(parameters.get("type")) && !parameters.containsKey("properties")) {
parameters.put("properties", new java.util.HashMap<>());
}

return parameters;
}

public String getDescription() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright 2023-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.springframework.ai.openai.api;

import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for FunctionTool.Function parameters validation.
*
* @author Liu Guodong
*/
class FunctionToolParametersTest {

private final ObjectMapper objectMapper = new ObjectMapper();

@Test
void testFunctionWithNoParameters() throws Exception {
// Test case 1: JSON schema with no properties field
String jsonSchemaNoProperties = """
{
"type": "object"
}
""";

OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function(
"Test function with no parameters", "test_function", jsonSchemaNoProperties);

assertThat(function.getParameters()).isNotNull();
assertThat(function.getParameters().get("type")).isEqualTo("object");
assertThat(function.getParameters().get("properties")).isNotNull();
assertThat(function.getParameters().get("properties")).isInstanceOf(Map.class);

// Verify serialization produces valid JSON
String json = this.objectMapper.writeValueAsString(function);
assertThat(json).contains("\"properties\"");
}

@Test
void testFunctionWithEmptyProperties() throws Exception {
// Test case 2: JSON schema with empty properties
String jsonSchemaEmptyProperties = """
{
"type": "object",
"properties": {}
}
""";

OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function(
"Test function with empty properties", "test_function", jsonSchemaEmptyProperties);

assertThat(function.getParameters()).isNotNull();
assertThat(function.getParameters().get("type")).isEqualTo("object");
assertThat(function.getParameters().get("properties")).isNotNull();

// Verify serialization produces valid JSON
String json = this.objectMapper.writeValueAsString(function);
assertThat(json).contains("\"properties\"");
}

@Test
void testFunctionWithParameters() throws Exception {
// Test case 3: JSON schema with actual parameters
String jsonSchemaWithParams = """
{
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "First parameter"
}
},
"required": ["param1"]
}
""";

OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function("Test function with parameters",
"test_function", jsonSchemaWithParams);

assertThat(function.getParameters()).isNotNull();
assertThat(function.getParameters().get("type")).isEqualTo("object");
assertThat(function.getParameters().get("properties")).isNotNull();
assertThat(function.getParameters().get("properties")).isInstanceOf(Map.class);

@SuppressWarnings("unchecked")
Map<String, Object> properties = (Map<String, Object>) function.getParameters().get("properties");
assertThat(properties).containsKey("param1");

// Verify serialization produces valid JSON
String json = this.objectMapper.writeValueAsString(function);
assertThat(json).contains("\"properties\"");
assertThat(json).contains("\"param1\"");
}

@Test
void testFunctionWithNullSchema() throws Exception {
// Test case 4: null JSON schema (edge case)
String nullSchema = null;
OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function("Test function with null schema",
"test_function", nullSchema);

// Should create a valid empty parameters object
assertThat(function.getParameters()).isNotNull();
assertThat(function.getParameters().get("type")).isEqualTo("object");
assertThat(function.getParameters().get("properties")).isNotNull();

// Verify serialization produces valid JSON
String json = this.objectMapper.writeValueAsString(function);
assertThat(json).contains("\"properties\"");
}

@Test
void testFunctionWithVoidTypeSchema() throws Exception {
// Test case 5: Schema generated for Void.class (common case for no-param
// functions)
// This simulates what JsonSchemaGenerator would produce
String voidSchema = """
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false
}
""";

OpenAiApi.FunctionTool.Function function = new OpenAiApi.FunctionTool.Function("Test function for Void type",
"test_void_function", voidSchema);

assertThat(function.getParameters()).isNotNull();
assertThat(function.getParameters().get("type")).isEqualTo("object");
assertThat(function.getParameters().get("properties")).isNotNull();
assertThat(function.getParameters().get("properties")).isInstanceOf(Map.class);

// Verify serialization produces valid JSON for OpenAI API
String json = this.objectMapper.writeValueAsString(function);
assertThat(json).contains("\"properties\"");
}

}