can1357/oh-my-pi · error · OmpTypeError

${kind} length must be a nonnegative safe integer

Error message

${kind} length must be a nonnegative safe integer

What it means

The zod-compat layer's `.min()`/`.max()` length methods validate the bound via `lengthBound`. For string and array schemas the bound must be a nonnegative safe integer; NaN, Infinity, negative values, or non-integers throw `<kind> length must be a nonnegative safe integer`.

Source

Thrown at packages/omptype/src/zod.ts:82

		hasDefault: false,
		run: value => value,
	};
	return type.raw(embedded) as unknown as Decoratable<Out>;
}

function restrictBase<Out>(source: Decoratable<Out>, ir: IR): Decoratable<Out> {
	let next = source.hasSteps
		? schemaFromIR<Out>({ k: "morph", input: ir, fn: value => source(value) })
		: schemaFromIR<Out>(ir);
	if (source.ir.desc !== undefined) next = next.describe(source.ir.desc);
	if (source.hasDefault) next = next.default(source.defaultValue as Out | (() => Out));
	return next;
}

function lengthBound(kind: "min" | "max", schema: Decoratable<unknown>, bound: number): void {
	if (schema.ir.k !== "string" && schema.ir.k !== "array") return;
	if (!Number.isSafeInteger(bound) || bound < 0) {
		throw new OmpTypeError(`${kind} length must be a nonnegative safe integer`);
	}
}

function refinementMessage(messageOrOptions: string | RefineOptions | undefined): string {
	if (typeof messageOrOptions === "string") return messageOrOptions;
	return messageOrOptions?.message ?? messageOrOptions?.error ?? "valid (refinement failed)";
}

function isStringKeyIR(ir: IR): boolean {
	switch (ir.k) {
		case "string":
			return true;
		case "lit":
			return typeof ir.v === "string";
		case "union":
			return ir.members.length > 0 && ir.members.every(isStringKeyIR);
		case "sub":
			return isStringKeyIR(ir.schema.ir);

View on GitHub (pinned to 9690622007)

Solutions

  1. Validate the bound first: `Number.isSafeInteger(bound) && bound >= 0`; round with `Math.max(0, Math.floor(x))` if deriving it.
  2. If the constraint should be unbounded, omit the `.min()`/`.max()` call rather than passing Infinity.
  3. Fix the source computation producing NaN/fractional values before it reaches the schema builder.

Example fix

// before
const minLen = Number(config.minLen); // NaN
schema.min(minLen);
// after
const raw = Number(config.minLen);
if (Number.isSafeInteger(raw) && raw >= 0) schema.min(raw);
Defensive patterns

Strategy: validation

Validate before calling

function nonNegInt(n: number): number {
  if (!Number.isSafeInteger(n) || n < 0) throw new RangeError(`length bound must be a nonnegative safe integer, got ${n}`);
  return n;
}
// use: schema.min(nonNegInt(rawMin))

Type guard

function isLengthBound(v: unknown): v is number {
  return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
}

Try / catch

try {
  const s = z.string().min(rawMin);
} catch (err) {
  if (err instanceof Error && err.message.includes('must be a nonnegative safe integer')) {
    throw new Error(`Invalid length bound ${rawMin} from config`);
  }
  throw err;
}

Prevention

When it happens

Trigger: `z.string().min(-1)`, `z.string().max(Infinity)`, `z.array(el).min(0.5)`, or bounds computed from variables (e.g. `Math.sqrt(-1)`, parsed config) yielding NaN/Infinity/fractions.

Common situations: Reading limits from unvalidated config/env where the parse fails to NaN; arithmetic like `Math.floor(total/2)` on undefined; copying zod v3 code that tolerated non-integer bounds.

Related errors


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