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

  1. Break the cycle: use JSON Schema `$ref`/`$defs` (string references) for recursive types instead of direct object references
  2. Deep-clone the schema before passing it to remove accidental shared references (structuredClone works only if it has no true cycles)
  3. If recursion is intentional and required, skip strict-mode enforcement with tryEnforceStrictSchema, which returns a failure instead of throwing
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/0647319d1f2ff266. Report an issue: GitHub.