can1357/oh-my-pi · error · AIError.ValidationError

Schema node has no type, combinator, or $ref — cannot enforc

Error message

Schema node has no type, combinator, or $ref — cannot enforce strict mode

What it means

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.

Source

Thrown at packages/ai/src/utils/schema/normalize.ts:2274

	// combinator / `$ref` / `not`). When `type` is missing, try to infer it
	// from a homogeneous-primitive `enum` / `const` so direct calls to
	// `enforceStrictSchema` (which bypass `sanitizeSchemaForStrictMode`'s own
	// inference pass) still produce wire-valid output.
	if (result.type === undefined) {
		const inferred = inferStrictPrimitiveTypeFromEnumOrConst(result);
		if (inferred !== undefined) result.type = inferred;
	}
	// Schemas like `{}`, `{items: {}}`, mixed-primitive enums, and non-primitive
	// consts are not representable in strict mode — `enum`/`const` are not
	// accepted as type substitutes here because they did not yield a single
	// inferable type above.
	if (
		result.type === undefined &&
		result.$ref === undefined &&
		!COMBINATOR_KEYS.some(key => Array.isArray(result[key])) &&
		!isJsonObject(result.not)
	) {
		throw new AIError.ValidationError("Schema node has no type, combinator, or $ref — cannot enforce strict mode");
	}
	return result;
}

export function tryEnforceStrictSchema(schema: Record<string, unknown>): {
	schema: Record<string, unknown>;
	strict: boolean;
} {
	return stamp(schema, kStrictSchema, s => {
		const upgraded = upgradeJsonSchemaTo202012(s) as Record<string, unknown>;
		if (hasUnrepresentableStrictObjectMap(upgraded)) {
			return { schema: upgraded, strict: false };
		}
		try {
			const sanitized = sanitizeSchemaForStrictMode(upgraded);
			return { schema: enforceStrictSchema(sanitized), strict: true };
		} catch {
			return { schema: upgraded, strict: false };

View on GitHub (pinned to 9690622007)

Solutions

  1. Give every schema node an explicit `type` (e.g. `"type": "object"`, `"type": "string"`)
  2. Wrap truly-any-typed nodes in a combinator such as `{ anyOf: [{type:"string"},{type:"number"},...] }`
  3. Use a `$ref` into `$defs` for shared/recursive nodes instead of empty placeholders
  4. Use tryEnforceStrictSchema to detect the offending node gracefully and fix it at build time

Example fix

// before
const schema = { type: "object", properties: { data: {} } };
// after
const schema = { type: "object", properties: { data: { type: "object", properties: {}, additionalProperties: true } } };
Defensive patterns

Strategy: validation

Validate before calling

function nodesHaveType(s: unknown): boolean {
  if (typeof s !== "object" || s === null || Array.isArray(s)) return true;
  const n = s as Record<string, unknown>;
  const ok = n.type !== undefined || n.$ref !== undefined || ["anyOf","oneOf","allOf"].some(k => Array.isArray(n[k])) || typeof n.not === "object" && n.not !== null;
  return ok && Object.values(n).every(nodesHaveType);
}
if (!nodesHaveType(schema)) throw new Error("schema node missing type");

Type guard

function isStrictableNode(n: Record<string, unknown>): boolean {
  return n.type !== undefined || n.$ref !== undefined ||
    ["anyOf","oneOf","allOf"].some(k => Array.isArray(n[k])) ||
    (typeof n.not === "object" && n.not !== null);
}

Try / catch

const res = tryEnforceStrictSchema(schema);
if (!res.schema) {
  console.error("strict enforcement failed:", res.error);
}
const strict = res.schema ?? schema;

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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