{"record":{"id":"4576cd55bdcdf0ce","repo":"can1357/oh-my-pi","slug":"schema-node-has-no-type-combinator-or-ref-can","errorCode":null,"errorMessage":"Schema node has no type, combinator, or $ref — cannot enforce strict mode","messagePattern":"Schema node has no type, combinator, or \\$ref — cannot enforce strict mode","errorType":"validation","errorClass":"AIError.ValidationError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/utils/schema/normalize.ts","lineNumber":2274,"sourceCode":"\t// combinator / `$ref` / `not`). When `type` is missing, try to infer it\n\t// from a homogeneous-primitive `enum` / `const` so direct calls to\n\t// `enforceStrictSchema` (which bypass `sanitizeSchemaForStrictMode`'s own\n\t// inference pass) still produce wire-valid output.\n\tif (result.type === undefined) {\n\t\tconst inferred = inferStrictPrimitiveTypeFromEnumOrConst(result);\n\t\tif (inferred !== undefined) result.type = inferred;\n\t}\n\t// Schemas like `{}`, `{items: {}}`, mixed-primitive enums, and non-primitive\n\t// consts are not representable in strict mode — `enum`/`const` are not\n\t// accepted as type substitutes here because they did not yield a single\n\t// inferable type above.\n\tif (\n\t\tresult.type === undefined &&\n\t\tresult.$ref === undefined &&\n\t\t!COMBINATOR_KEYS.some(key => Array.isArray(result[key])) &&\n\t\t!isJsonObject(result.not)\n\t) {\n\t\tthrow new AIError.ValidationError(\"Schema node has no type, combinator, or $ref — cannot enforce strict mode\");\n\t}\n\treturn result;\n}\n\nexport function tryEnforceStrictSchema(schema: Record<string, unknown>): {\n\tschema: Record<string, unknown>;\n\tstrict: boolean;\n} {\n\treturn stamp(schema, kStrictSchema, s => {\n\t\tconst upgraded = upgradeJsonSchemaTo202012(s) as Record<string, unknown>;\n\t\tif (hasUnrepresentableStrictObjectMap(upgraded)) {\n\t\t\treturn { schema: upgraded, strict: false };\n\t\t}\n\t\ttry {\n\t\t\tconst sanitized = sanitizeSchemaForStrictMode(upgraded);\n\t\t\treturn { schema: enforceStrictSchema(sanitized), strict: true };\n\t\t} catch {\n\t\t\treturn { schema: upgraded, strict: false };","sourceCodeStart":2256,"sourceCodeEnd":2292,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/utils/schema/normalize.ts#L2256-L2292","documentation":"enforceStrictSchema's per-node pass requires every schema node to be one of: typed (type set), a combinator (anyOf/oneOf/allOf array present), a $ref, or a `not` object. A node with none of these (e.g. `{}` or `{ description: \"...\" }`) cannot be turned into a strict-mode schema, so AIError.ValidationError is thrown.","triggerScenarios":"Registering a tool whose parameters schema contains an empty object node (`{}`), a node with only annotations (description/title/default) and no type, or a boolean-ish placeholder object, when the provider path enforces strict schema normalization.","commonSituations":"Hand-written tool schemas with placeholder nodes left empty; code generators emitting `{}` for unknown/any types; schemas migrated from draft formats where an empty object means 'anything' but strict providers reject that; spreading optional fragments that drop the `type` key.","solutions":["Give every schema node an explicit `type` (e.g. `\"type\": \"object\"`, `\"type\": \"string\"`)","Wrap truly-any-typed nodes in a combinator such as `{ anyOf: [{type:\"string\"},{type:\"number\"},...] }`","Use a `$ref` into `$defs` for shared/recursive nodes instead of empty placeholders","Use tryEnforceStrictSchema to detect the offending node gracefully and fix it at build time"],"exampleFix":"// before\nconst schema = { type: \"object\", properties: { data: {} } };\n// after\nconst schema = { type: \"object\", properties: { data: { type: \"object\", properties: {}, additionalProperties: true } } };","handlingStrategy":"validation","validationCode":"function nodesHaveType(s: unknown): boolean {\n  if (typeof s !== \"object\" || s === null || Array.isArray(s)) return true;\n  const n = s as Record<string, unknown>;\n  const ok = n.type !== undefined || n.$ref !== undefined || [\"anyOf\",\"oneOf\",\"allOf\"].some(k => Array.isArray(n[k])) || typeof n.not === \"object\" && n.not !== null;\n  return ok && Object.values(n).every(nodesHaveType);\n}\nif (!nodesHaveType(schema)) throw new Error(\"schema node missing type\");","typeGuard":"function isStrictableNode(n: Record<string, unknown>): boolean {\n  return n.type !== undefined || n.$ref !== undefined ||\n    [\"anyOf\",\"oneOf\",\"allOf\"].some(k => Array.isArray(n[k])) ||\n    (typeof n.not === \"object\" && n.not !== null);\n}","tryCatchPattern":"const res = tryEnforceStrictSchema(schema);\nif (!res.schema) {\n  console.error(\"strict enforcement failed:\", res.error);\n}\nconst strict = res.schema ?? schema;","preventionTips":["Never leave placeholder `{}` nodes in tool parameter schemas","Give every property an explicit type or wrap in anyOf","Run tryEnforceStrictSchema in tests before shipping tool definitions","Enable strict provider settings only with fully typed schemas"],"tags":["schema","json-schema","strict-mode","validation"],"backgroundTag":"schema-node-missing-type","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}