Skip to content

allOf lowers to v.intersect([v.strictObject, v.strictObject]), which rejects every value #18

@maxholman

Description

@maxholman

Version: 10.0.5 (also 10.0.3 / 10.0.4)
File: lib/valibot.ts:337-354 and :357-400

Summary

For an OpenAPI allOf whose members are objects, the valibot emitter writes v.intersect([strictObject(A), strictObject(B), …]). Because each strictObject independently rejects unknown keys, any value with properties from one member but not the other fails validation. With two disjoint-property members the schema rejects every possible value.

Minimal repro

OpenAPI (mirrors what @block65/openapi-constructs emits from Schema(..., { schema: { allOf: [...] } })):

components:
  schemas:
    Org:
      type: object
      required: [id, name]
      properties:
        id:   { type: string }
        name: { type: string }
    UserMembership:
      allOf:
        - $ref: '#/components/schemas/Org'
        - type: object
          required: [role]
          properties:
            role: { type: string }

Generated valibot.ts:

export const orgSchema = v.strictObject({ id: v.string(), name: v.string() });
export const userMembershipSchema = v.intersect([
  orgSchema,
  v.strictObject({ role: v.string() }),
]);

Calling v.parse(userMembershipSchema, { id: "o1", name: "Acme", role: "admin" }) throws:

ValiError: Invalid key: Expected never but received "role"
ValiError: Invalid key: Expected never but received "id"

The first strictObject rejects role; the second rejects id and name. There is no input that satisfies both.

Expected

allOf of object schemas should be merged into a single v.strictObject with the union of properties (and union of required). The OpenAPI spec defines allOf as schema composition (logical AND), and every other major OpenAPI→validator toolchain (zod-openapi, openapi-zod-client, redocly, etc.) collapses allOf of objects into one merged object before emitting the closed-object validator.

Actual

lib/valibot.ts:342 unconditionally picks intersect for allOf, then :400 emits each object member as a strictObject. The two are incompatible in valibot — v.intersect([strictObject, strictObject]) is only viable if the property sets are identical.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions