{"record":{"id":"0647319d1f2ff266","repo":"can1357/oh-my-pi","slug":"schema-contains-a-circular-object-graph-cannot-e","errorCode":null,"errorMessage":"Schema contains a circular object graph — cannot enforce strict mode","messagePattern":"Schema contains a circular object graph — cannot enforce strict mode","errorType":"validation","errorClass":"AIError.ValidationError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/utils/schema/normalize.ts","lineNumber":2128,"sourceCode":" * Recursively enforces JSON Schema constraints required by OpenAI/Codex strict mode:\n *   - `additionalProperties: false` on every object node\n *   - every key in `properties` present in `required`\n *\n * Properties absent from the original `required` array were TypeBox-optional.\n * They are made nullable (`anyOf: [T, { type: \"null\" }]`) so the model can\n * signal omission by outputting null rather than omitting the key entirely.\n *\n * @throws {Error} When a schema node has no `type`, array-based combinator\n *   (`anyOf`/`allOf`/`oneOf`), object-based combinator (`not`), or `$ref` —\n *   i.e. the node is not representable in strict mode. Prefer\n *   {@link tryEnforceStrictSchema} which catches this and degrades gracefully.\n */\nexport function enforceStrictSchema(\n\tschema: Record<string, unknown>,\n\tcache: WeakMap<Record<string, unknown>, Record<string, unknown>> = new WeakMap(),\n): Record<string, unknown> {\n\tif (!enter(schema)) {\n\t\tthrow new AIError.ValidationError(\"Schema contains a circular object graph — cannot enforce strict mode\");\n\t}\n\ttry {\n\t\tconst cached = cache.get(schema);\n\t\tif (cached) return cached;\n\t\tconst result = { ...schema };\n\t\tcache.set(schema, result);\n\t\treturn enforceStrictSchemaBody(schema, result, cache);\n\t} finally {\n\t\texit(schema);\n\t}\n}\n\nfunction enforceStrictSchemaBody(\n\t_schema: Record<string, unknown>,\n\tresult: Record<string, unknown>,\n\tcache: WeakMap<Record<string, unknown>, Record<string, unknown>>,\n): Record<string, unknown> {\n\tconst isObjectType = result.type === \"object\";","sourceCodeStart":2110,"sourceCodeEnd":2146,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/utils/schema/normalize.ts#L2110-L2146","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before — cycle: node.properties.self === node\nnode.properties = { self: node };\nenforceStrictSchema(node);\n// after — use $ref for recursion\nconst schema = {\n  type: \"object\",\n  $defs: { node: { type: \"object\", properties: { self: { $ref: \"#/$defs/node\" } } } },\n  $ref: \"#/$defs/node\",\n};\nenforceStrictSchema(schema);","handlingStrategy":"validation","validationCode":"function hasCycle(obj: object, seen = new WeakSet()): boolean {\n  if (typeof obj !== \"object\" || obj === null) return false;\n  if (seen.has(obj)) return true;\n  seen.add(obj);\n  return Object.values(obj).some(v => hasCycle(v, seen));\n}\nif (hasCycle(schema)) useRefsInstead(schema);","typeGuard":"function isAcyclicSchema(s: Record<string, unknown>): boolean {\n  try { JSON.stringify(s); return true; } catch { return false; }\n}","tryCatchPattern":"let strictSchema;\ntry {\n  strictSchema = enforceStrictSchema(schema);\n} catch (err) {\n  if (err instanceof AIError.ValidationError) {\n    strictSchema = schema; // fall back to non-strict\n  } else throw err;\n}","preventionTips":["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"],"tags":["schema","json-schema","strict-mode","validation"],"backgroundTag":"circular-schema","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}