Describe the bug
JsonSchemaUtils initializes a single victools SchemaGenerator in a static
block (agentscope-core/src/main/java/io/agentscope/core/util/JsonSchemaUtils.java,
lines 62–84) and shares it across the whole JVM without any synchronization.
When multiple threads perform structured calls targeting the same output class
(e.g. multiple agents running in parallel), generateSchemaFromClass() /
generateSchemaFromType() can fail with a ConcurrentModificationException
wrapped in a RuntimeException.
The failure is timing-sensitive and intermittent: in our production setup (two
expert agents reviewing in parallel), it triggered once across many runs — the
losing agent fails immediately at its first structured call and returns an
ERROR, while the other agent completes normally.
To Reproduce
Steps to reproduce the behavior:
- Run two (or more)
ReActAgent instances in parallel threads, both performing
a structured call with the same target class:
// thread A
agentA.call(messages, MyReport.class, context);
// thread B, overlapping in time
agentB.call(messages, MyReport.class, context);
- Any overlap of two
doStructuredCall invocations that both reach
JsonSchemaUtils.generateSchemaFromClass(...) for a not-yet-introspected
class races on the shared SchemaGenerator.
- Occasionally one thread throws (see error below).
Note: the race window is narrow — two threads must be introspecting the same
not-yet-cached class at the same time. We could not reproduce it
deterministically locally (800 concurrent calls across two stress rounds stayed
green), which matches the intermittent production behavior. The race is still
real: there is no happens-before edge between concurrent calls.
Expected behavior
Schema generation from shared static state should be thread-safe (or the class
should be explicitly documented as not thread-safe). Concurrent structured
calls are a normal usage pattern for multi-agent applications.
Error messages
java.lang.RuntimeException: Failed to generate JSON schema for com.example.MyStructuredOutput
at io.agentscope.core.util.JsonSchemaUtils.generateSchemaFromClass(JsonSchemaUtils.java:102)
at io.agentscope.core.ReActAgent.doStructuredCall(ReActAgent.java:1054)
...
Caused by: java.util.ConcurrentModificationException
at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1013)
at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector._sortProperties(POJOPropertiesCollector.java:1868)
at com.fasterxml.jackson.databind.introspect.POJOPropertiesCollector.collectAll(POJOPropertiesCollector.java:502)
at com.fasterxml.jackson.databind.introspect.BasicBeanDescription.findProperties(BasicBeanDescription.java:236)
at com.github.victools.jsonschema.module.jackson.JacksonModule.shouldIgnoreField(JacksonModule.java:306)
at com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart.lambda$shouldIgnore$0(SchemaGeneratorConfigPart.java:135)
(stack trace trimmed to the relevant frames)
Environment (please complete the following information):
- AgentScope-Java Version: 2.0.1 (also verified the code is unchanged on latest
main, commit 0de9ed43)
- Java Version: 17
- OS: macOS
Additional context
Root cause analysis:
- Shared singleton without synchronization —
JsonSchemaUtils holds a
private static final SchemaGenerator (one instance per JVM); neither
generateSchemaFromClass nor generateSchemaFromType guards the
schemaGenerator.generateSchema(...) call.
- victools
SchemaGenerator is not thread-safe — a known limitation of
victools/jsonschema-generator. In jsonschema-module-jackson 4.38.0,
JacksonModule keeps an instance-level Map<Class, BeanDescription>
introspection cache (a plain HashMap, no concurrency protection).
Concurrent getBeanDescriptionForClass calls can share one
BeanDescription, and two threads racing on its lazily-populated
POJOPropertiesCollector (one iterating in _sortProperties while the
other inserts) throw the CME above.
- Wide trigger surface —
ReActAgent.doStructuredCall regenerates the
schema on every structured call (no caching), and the
call(msgs, Class, ctx) entry always goes through the shared generator.
So any two overlapping structured calls — not just agent startup, any ReAct
iteration — form a data race. CME is only the most visible symptom; without
a happens-before edge, the shared HashMap cache can also silently lose
entries or get structurally corrupted.
The same class of bug was reported and fixed in Spring AI:
spring-ai#6207.
The Spring AI team resolved it by guarding every SchemaGenerator.generateSchema(...)
call with synchronized (commit 9b8eb07, "Guard JSON schema generation"),
plus concurrent regression tests.
Suggested fix: apply the same minimal guard in JsonSchemaUtils — wrap the two
schemaGenerator.generateSchema(...) call sites in a synchronized block
(a dedicated lock object, or synchronized (schemaGenerator) as Spring AI did).
This keeps the public API unchanged. A schema cache could be a separate
enhancement later.
Happy to submit a PR for this.
Describe the bug
JsonSchemaUtilsinitializes a single victoolsSchemaGeneratorin a staticblock (
agentscope-core/src/main/java/io/agentscope/core/util/JsonSchemaUtils.java,lines 62–84) and shares it across the whole JVM without any synchronization.
When multiple threads perform structured calls targeting the same output class
(e.g. multiple agents running in parallel),
generateSchemaFromClass()/generateSchemaFromType()can fail with aConcurrentModificationExceptionwrapped in a
RuntimeException.The failure is timing-sensitive and intermittent: in our production setup (two
expert agents reviewing in parallel), it triggered once across many runs — the
losing agent fails immediately at its first structured call and returns an
ERROR, while the other agent completes normally.
To Reproduce
Steps to reproduce the behavior:
ReActAgentinstances in parallel threads, both performinga structured call with the same target class:
doStructuredCallinvocations that both reachJsonSchemaUtils.generateSchemaFromClass(...)for a not-yet-introspectedclass races on the shared
SchemaGenerator.Note: the race window is narrow — two threads must be introspecting the same
not-yet-cached class at the same time. We could not reproduce it
deterministically locally (800 concurrent calls across two stress rounds stayed
green), which matches the intermittent production behavior. The race is still
real: there is no happens-before edge between concurrent calls.
Expected behavior
Schema generation from shared static state should be thread-safe (or the class
should be explicitly documented as not thread-safe). Concurrent structured
calls are a normal usage pattern for multi-agent applications.
Error messages
(stack trace trimmed to the relevant frames)
Environment (please complete the following information):
main, commit0de9ed43)Additional context
Root cause analysis:
JsonSchemaUtilsholds aprivate static final SchemaGenerator(one instance per JVM); neithergenerateSchemaFromClassnorgenerateSchemaFromTypeguards theschemaGenerator.generateSchema(...)call.SchemaGeneratoris not thread-safe — a known limitation ofvictools/jsonschema-generator. In
jsonschema-module-jackson4.38.0,JacksonModulekeeps an instance-levelMap<Class, BeanDescription>introspection cache (a plain
HashMap, no concurrency protection).Concurrent
getBeanDescriptionForClasscalls can share oneBeanDescription, and two threads racing on its lazily-populatedPOJOPropertiesCollector(one iterating in_sortPropertieswhile theother inserts) throw the CME above.
ReActAgent.doStructuredCallregenerates theschema on every structured call (no caching), and the
call(msgs, Class, ctx)entry always goes through the shared generator.So any two overlapping structured calls — not just agent startup, any ReAct
iteration — form a data race. CME is only the most visible symptom; without
a happens-before edge, the shared HashMap cache can also silently lose
entries or get structurally corrupted.
The same class of bug was reported and fixed in Spring AI:
spring-ai#6207.
The Spring AI team resolved it by guarding every
SchemaGenerator.generateSchema(...)call with
synchronized(commit9b8eb07, "Guard JSON schema generation"),plus concurrent regression tests.
Suggested fix: apply the same minimal guard in
JsonSchemaUtils— wrap the twoschemaGenerator.generateSchema(...)call sites in asynchronizedblock(a dedicated lock object, or
synchronized (schemaGenerator)as Spring AI did).This keeps the public API unchanged. A schema cache could be a separate
enhancement later.
Happy to submit a PR for this.