can1357/oh-my-pi · error · AIError.ValidationError
Schema contains a circular object graph — cannot enforce str
Error message
Schema contains a circular object graph — cannot enforce strict mode
What it means
enforceStrictSchema converts a JSON Schema into a strict-mode schema (e.g. for providers requiring strict structured output). It first walks the schema with enter() to detect cycles; a circular object graph means a node references itself, which the recursive strict-mode transform cannot terminate on. The library throws AIError.ValidationError rather than looping forever or silently producing an invalid schema.
Source
Thrown at packages/ai/src/utils/schema/normalize.ts:2128
* Recursively enforces JSON Schema constraints required by OpenAI/Codex strict mode:
* - `additionalProperties: false` on every object node
* - every key in `properties` present in `required`
*
* Properties absent from the original `required` array were TypeBox-optional.
* They are made nullable (`anyOf: [T, { type: "null" }]`) so the model can
* signal omission by outputting null rather than omitting the key entirely.
*
* @throws {Error} When a schema node has no `type`, array-based combinator
* (`anyOf`/`allOf`/`oneOf`), object-based combinator (`not`), or `$ref` —
* i.e. the node is not representable in strict mode. Prefer
* {@link tryEnforceStrictSchema} which catches this and degrades gracefully.
*/
export function enforceStrictSchema(
schema: Record<string, unknown>,
cache: WeakMap<Record<string, unknown>, Record<string, unknown>> = new WeakMap(),
): Record<string, unknown> {
if (!enter(schema)) {
throw new AIError.ValidationError("Schema contains a circular object graph — cannot enforce strict mode");
}
try {
const cached = cache.get(schema);
if (cached) return cached;
const result = { ...schema };
cache.set(schema, result);
return enforceStrictSchemaBody(schema, result, cache);
} finally {
exit(schema);
}
}
function enforceStrictSchemaBody(
_schema: Record<string, unknown>,
result: Record<string, unknown>,
cache: WeakMap<Record<string, unknown>, Record<string, unknown>>,
): Record<string, unknown> {
const isObjectType = result.type === "object";View on GitHub (pinned to 9690622007)
Solutions
- Break the cycle: use JSON Schema `$ref`/`$defs` (string references) for recursive types instead of direct object references
- Deep-clone the schema before passing it to remove accidental shared references (structuredClone works only if it has no true cycles)
- If recursion is intentional and required, skip strict-mode enforcement with tryEnforceStrictSchema, which returns a failure instead of throwing
- Verify with a cycle-detection utility (or JSON.stringify, which throws on cycles) before submitting
Example fix
// before — cycle: node.properties.self === node
node.properties = { self: node };
enforceStrictSchema(node);
// after — use $ref for recursion
const schema = {
type: "object",
$defs: { node: { type: "object", properties: { self: { $ref: "#/$defs/node" } } } },
$ref: "#/$defs/node",
};
enforceStrictSchema(schema); Defensive patterns
Strategy: validation
Validate before calling
function hasCycle(obj: object, seen = new WeakSet()): boolean {
if (typeof obj !== "object" || obj === null) return false;
if (seen.has(obj)) return true;
seen.add(obj);
return Object.values(obj).some(v => hasCycle(v, seen));
}
if (hasCycle(schema)) useRefsInstead(schema); Type guard
function isAcyclicSchema(s: Record<string, unknown>): boolean {
try { JSON.stringify(s); return true; } catch { return false; }
} Try / catch
let strictSchema;
try {
strictSchema = enforceStrictSchema(schema);
} catch (err) {
if (err instanceof AIError.ValidationError) {
strictSchema = schema; // fall back to non-strict
} else throw err;
} Prevention
- Model recursive types with $ref/$defs, never with object self-references
- Deep-clone programmatically assembled schemas before enforcement
- Prefer tryEnforceStrictSchema where strict mode is optional
- Keep schema fragments immutable so shared references cannot create cycles
When it happens
Trigger: Passing a schema object that contains a self-referencing structure — e.g. a node's property or items array containing (directly or indirectly) the same object instance — into enforceStrictSchema, or indirectly via buildRequest/tool registration when strict mode is enforced.
Common situations: Programmatically building recursive schemas by mutating objects so a child points back at a parent; caching/memoizing schema fragments and re-inserting the same object reference into multiple places creating a cycle; JSON Schema definitions using $ref-like indirection implemented with actual object references instead of $ref strings.
Related errors
- Schema node has no type, combinator, or $ref — cannot enforc
- anthropic-messages: ${data.summary}
- Validation failed for tool "${toolCall.name}":\n${errors}\n\
- ClinePass model catalog response is missing clinePass
- rewrite received invalid arguments: ${params.summary}
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/0647319d1f2ff266.
Report an issue: GitHub.