diff --git a/agentscope-core/src/main/java/io/agentscope/core/util/JsonSchemaUtils.java b/agentscope-core/src/main/java/io/agentscope/core/util/JsonSchemaUtils.java
index f15efe5984..30e620d2db 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/util/JsonSchemaUtils.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/util/JsonSchemaUtils.java
@@ -29,6 +29,7 @@
import io.agentscope.core.tool.ToolSchemaModule;
import java.lang.reflect.Type;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
/**
* Utility class for JSON Schema operations.
@@ -53,6 +54,11 @@
*
{@code @JsonClassDescription(...)} - add class description
*
*
+ * All public methods are thread-safe. Schema generation through the shared victools
+ * {@code SchemaGenerator} is serialized by an internal lock, because the generator itself
+ * is not designed for concurrent use. Generated schemas are cached per {@link Class}/{@link Type}
+ * so that the lock is only needed the first time a given class or type is seen.
+ *
* @hidden
*/
public class JsonSchemaUtils {
@@ -61,6 +67,29 @@ public class JsonSchemaUtils {
private static final SchemaGenerator schemaGenerator;
+ /**
+ * Guards the shared victools {@link SchemaGenerator}, which is not thread-safe: its
+ * JacksonModule keeps an unsynchronized introspection cache, so concurrent schema
+ * generation must be serialized. Only cache misses in {@link #CLASS_SCHEMA_CACHE} and
+ * {@link #TYPE_SCHEMA_CACHE} take this lock.
+ */
+ private static final Object SCHEMA_LOCK = new Object();
+
+ /**
+ * Caches the schema {@link JsonNode} generated for each class, since it is a deterministic
+ * function of the class and the static, never-changing generator config, so no invalidation
+ * is needed. Values are never mutated after being cached; every call still converts a fresh,
+ * independently mutable {@code Map} from the cached node. Unbounded, but keys are the
+ * compile-time-fixed structured-output and tool-parameter classes declared by application
+ * code, so the entry count is bounded by the (small, finite) set of classes the JVM loads for
+ * that purpose, not by request volume or untrusted input.
+ */
+ private static final Map, JsonNode> CLASS_SCHEMA_CACHE = new ConcurrentHashMap<>();
+
+ /** Same caching strategy and bound rationale as {@link #CLASS_SCHEMA_CACHE}, keyed by
+ * generic {@link Type}. */
+ private static final Map TYPE_SCHEMA_CACHE = new ConcurrentHashMap<>();
+
static {
// JacksonModule to support @JsonProperty, @JsonPropertyDescription annotations
JacksonModule jacksonModule =
@@ -95,7 +124,14 @@ public class JsonSchemaUtils {
*/
public static Map generateSchemaFromClass(Class> clazz) {
try {
- JsonNode schemaNode = schemaGenerator.generateSchema(clazz);
+ JsonNode schemaNode =
+ CLASS_SCHEMA_CACHE.computeIfAbsent(
+ clazz,
+ c -> {
+ synchronized (SCHEMA_LOCK) {
+ return schemaGenerator.generateSchema(c);
+ }
+ });
return JsonUtils.getJsonCodec()
.convertValue(schemaNode, new TypeReference