Skip to content

Consider changing input type of object schemas with Valibot v2 #1389

@fabian-hiller

Description

@fabian-hiller

Discussed in #1366

Originally posted by mrft December 2, 2025
I'll start with a simple example:

  const SimpleSchema = V.object({ x: V.number() });
  type TSimpleInput = V.InferInput<typeof SimpleSchema>; // { x: number; } but in practice { x: number; [key: string]: any; }
  type TSimpleOutput = V.InferOutput<typeof SimpleSchema>; // { x: number; }
  console.debug(
    "simple object parse result:",
    V.safeParse(SimpleSchema, { x: 5, y: 7 })
  );

So the inferred input type is stricter than what safeParse will allow.

This bug (in my opinion) is also reflected in the intersection docs: https://valibot.dev/guides/intersections/, which further suggests that the implementation is not in line with people's intuition about how it should work:

These docs use the following example:

// TypeScript
type Intersect = { foo: string } & { bar: number };

// Valibot
const IntersectSchema = v.intersect([
  v.object({ foo: v.string() }),
  v.object({ bar: v.number() }),
]);

but in reality we get:

// TypeScript
type Intersect = { foo: string; [key: string]: any; } & { bar: number; [key: string]: any; };

// Valibot
const IntersectSchema = v.intersect([
  v.object({ foo: v.string() }),
  v.object({ bar: v.number(); }),
]);

because when parsing, it will not complain about extra properties, whereas the typescript compiler would complain about extra properties in the first version.

This gets worse when using a strictObject:

  const firstSchema = v.strictObject({
    a: v.number(),
    b: v.string(),
  });
  type TFirstInput = v.InferInput<typeof firstSchema>; // { a: number; b: string }

  const secondSchema = v.strictObject({
    c: v.number(),
  });
  type TSecondInput = v.InferInput<typeof secondSchema>; // { c: number; }

  const intersectSchema = v.intersect([firstSchema, secondSchema]);
  type TIntersectInput = v.InferInput<typeof intersectSchema>; // { a: number; b: string; c: number; }

  console.debug(
    "intersectSchema parse result:",
    v.safeParse(intersectSchema, { a: 1, b: "foo", c: 3 })
  ); // => {
  // typed: false,
  // success: false,
  // output: { a: 1, b: "foo", c: 3 },
  // issues: [
  //     'Invalid key: Expected never but received "c"',
  //     'Invalid key: Expected never but received "a"',
  // ]  

This is not inline with the inferred types, but also unintuitive (as well as useless) from a user perspective.

(Unfortunately zod has the exact same bug)

REMARK: if this is how it is supposed to work, then toJsonSchema should also be updated to avoid simply producing { allOf: [ { additionalProperties: false }, { ... } ] } as that would have the same problem (it should be { type: "object", unevaluatedProperties: false, allOf [ { ... additionalProperties undefined }, { ... additionalProperties undefined } ] } without the additionalProperties key, not supported in JSON-Schema draft‑07 but introduced since the JSON Schema 2019‑09 specs).

I let Github Copilot crunch on the issue to make it behave the way I would expect it to behave: https://github.com/mrft/valibot/tree/feat-intersect-strict

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestintendedThe behavior is expectednext versionSomething to look at for our next major release

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions