Skip to content

@pcvl/schema / ObjectInterpretedSchema

Class: ObjectInterpretedSchema<Shape, Input, Output> ​

Defined in: schemas/objectInterpretedSchema.ts:185

Validates each declared key against its own field schema, composing those schemas into one object type. Build one with pvl.object(shape).

Every field is checked even after an earlier one fails, so a single Result carries one Issue per failing field rather than one per round-trip. Each field runs through its own full pipeline — its .optional(), .nullable(), .coerce(), .refine(), .transform() — with the field's key appended to the path.

Unknown keys are stripped by default, so the output holds the declared keys only; .strict() reports them as issues instead and .passthrough() keeps them. There is no .coerce(): there is no unambiguous way to read an object out of a non-object. A field that needs coercion opts into it on its own schema.

Every modifier hands back an object schema rather than the base ChainableInterpretedSchema, so a modified object schema is still something pvl.compile() accepts, and .strict()/.passthrough() carry an earlier modifier's types through. .transform() ends the chain.

Example ​

ts
import { pvl } from '@pcvl/schema';

const user = pvl.object({
  name: pvl.string().min(1),
  age: pvl.number().int().min(0),
  nickname: pvl.string().optional(),
});

user.validate({ name: 'Ada', age: 36, extra: true });
// { value: { name: 'Ada', age: 36 } } — `extra` is stripped

user.validate({ name: '', age: 1.5 });
// { issues: [{ path: ['name'], ... }, { path: ['age'], ... }] } — both fields reported

Extends ​

Type Parameters ​

Shape ​

Shape extends ObjectShape = ObjectShape

Input ​

Input = ObjectInput<Shape>

Output ​

Output = ObjectOutput<Shape>

Accessors ​

shape ​

Get Signature ​

ts
get shape(): StandaloneShape<Shape>;

Defined in: schemas/objectInterpretedSchema.ts:259

The schema of each Standalone Key of this object schema, the fields marked .standalone(), so a single field can be reached and validated on its own without validating the whole object. Any other key is absent, and reading it is a compile error; an object with none has {}. A nested object's own shape follows the same rule.

Read-only: neither the shape nor any one field can be replaced, and reading a field never affects how this schema validates. It survives every modifier, .standalone() and pvl.compile(), but not .transform(), whose value needn't be an object.

Example ​
ts
import { pvl } from '@pcvl/schema';

const user = pvl.object({
  name: pvl.string().min(1).standalone(),
  address: pvl.object({ city: pvl.string().standalone() }).standalone(),
  age: pvl.number(),
});

user.shape.name.validate('Ada'); // { value: 'Ada' }
user.shape.name.validate(''); // { issues: [{ code: 'TOO_SMALL', ... }] }
user.shape.address.shape.city.validate('London'); // { value: 'London' }
// user.shape.age — a compile error: `age` isn't a Standalone Key
Returns ​

StandaloneShape<Shape>

Methods ​

nullable() ​

ts
nullable(): ObjectInterpretedSchema<Shape, Input | null, Output | null>;

Defined in: schemas/chainableInterpretedSchema.ts:78

Accepts null in addition to whatever this schema already accepts. Unlike .optional(), the key stays required — null has to be passed explicitly. Chain both to accept either.

Returns ​

ObjectInterpretedSchema<Shape, Input | null, Output | null>

Example ​

ts
import { pvl } from '@pcvl/schema';

const deletedAt = pvl.string().nullable();
deletedAt.validate(null); // { value: null }

const eitherWay = pvl.string().nullable().optional();

Inherited from ​

ChainableInterpretedSchema.nullable


optional() ​

ts
optional(): ObjectInterpretedSchema<Shape, Input | undefined, Output | undefined>;

Defined in: schemas/chainableInterpretedSchema.ts:54

Accepts undefined in addition to whatever this schema already accepts. As an object field, it also makes the key itself optional: an omitted key stays omitted from the output rather than becoming an explicit undefined property.

Returns ​

ObjectInterpretedSchema<Shape, Input | undefined, Output | undefined>

Example ​

ts
import { pvl } from '@pcvl/schema';

const user = pvl.object({ name: pvl.string(), nickname: pvl.string().optional() });

user.validate({ name: 'Ada' }); // { value: { name: 'Ada' } }
user.validate({ name: 'Ada', nickname: 'Addie' }); // both keys kept

Inherited from ​

ChainableInterpretedSchema.optional


passthrough() ​

ts
passthrough(): ObjectInterpretedSchema<Shape, Input, WithUnknownKeys<Output>>;

Defined in: schemas/objectInterpretedSchema.ts:309

Keeps the keys the shape does not declare instead of stripping them, and adds them to the output type as an unknown-valued index signature, so reading one still forces a narrowing step. It undoes an earlier .strict().

It sets the mode for the whole schema rather than at its place in the chain: every .refine() sees the undeclared keys, whether chained before or after it.

Returns ​

ObjectInterpretedSchema<Shape, Input, WithUnknownKeys<Output>>

Example ​

ts
import { pvl } from '@pcvl/schema';

const envelope = pvl.object({ id: pvl.string() }).passthrough();

envelope.validate({ id: 'a1', meta: { source: 'api' } });
// { value: { id: 'a1', meta: { source: 'api' } } }

refine() ​

ts
refine<Refined>(predicate, options?): ObjectInterpretedSchema<Shape, Input, RefinedOutput<Output, Refined>>;

Defined in: schemas/chainableInterpretedSchema.ts:134

Attaches a custom check. The predicate never changes the value; returning false produces an Issue with code CUSTOM. This is where constraints the library has no built-in for — a regex, a cross-field rule — belong.

It runs once the value has passed this schema's type check, in the order it was chained among the constraints, so a failing constraint before it does not stop it and both issues are reported. It is skipped for a value .optional() or .nullable() accepted.

When the predicate is a type predicate, written out ((value): value is T) or inferred by TypeScript ((value) => value === 'ahoj'), the output narrows to its type; a predicate returning a plain boolean leaves it as it was. The input never narrows. A null or undefined that .nullable() or .optional() chained before lets past the predicate stays in the output.

The schema comes back as the same type it went in as, so a refined object or array schema is still one, with its shape or element.

Type Parameters ​

Refined ​

Refined = Output

Parameters ​

predicate ​

((value) => boolean) | ((value) => value is Refined)

options? ​

Partial<Pick<IssueProps, "message">>

Returns ​

ObjectInterpretedSchema<Shape, Input, RefinedOutput<Output, Refined>>

Example ​

ts
import { pvl } from '@pcvl/schema';

const evenNumber = pvl
  .number()
  .refine((value) => value % 2 === 0, { message: 'must be even' });

evenNumber.validate(3); // { issues: [{ code: 'CUSTOM', message: 'must be even' }] }

// Output 'ahoj', input still string.
const greeting = pvl.string().refine((value) => value === 'ahoj');
// Output string | null: a null never reaches the predicate.
const name = pvl
  .string()
  .nullable()
  .refine((value) => value !== null);

// Still an object schema, so `shape` is still there.
const range = pvl
  .object({ min: pvl.number().standalone(), max: pvl.number() })
  .refine((value) => value.min <= value.max);
range.shape.min.validate(1); // { value: 1 }

Inherited from ​

ChainableInterpretedSchema.refine


standalone() ​

ts
standalone(): ObjectInterpretedSchema<Shape, Input, Output> & StandaloneMarker;

Defined in: schemas/interpretedSchema.ts:119

Marks this schema as a Standalone Key: as a field of pvl.object(), it is kept in that object's shape, so it can be validated on its own, and once the object is compiled, @pcvl/compiler gives it a compiled validator of its own. Validation is unchanged.

The schema comes back as the same type it went in as, marked, so the chain goes on: modifiers can follow it, and an object keeps its shape and an array its element. It is valid on any schema, a field or not, and calling it again changes nothing.

Returns ​

ObjectInterpretedSchema<Shape, Input, Output> & StandaloneMarker

Example ​

ts
import { pvl } from '@pcvl/schema';

const user = pvl.object({
  email: pvl.string().standalone().min(3),
  age: pvl.number().int(),
});

user.shape.email.validate('a@b.c'); // { value: 'a@b.c' }
// user.shape.age — a compile error: `age` isn't a Standalone Key

Inherited from ​

ChainableInterpretedSchema.standalone


strict() ​

ts
strict(options?): this;

Defined in: schemas/objectInterpretedSchema.ts:283

Reports keys the shape does not declare as Issues instead of stripping them — one UNRECOGNIZED_KEY issue per extra key, pathed to that key. Use it where an unexpected key means a typo or a stale caller rather than harmless extra data.

An object with a failing field reports only the field issues; its unknown keys are reported once every field passes. .strict() and .passthrough() are mutually exclusive; the last one chained wins.

Parameters ​

options? ​

Partial<Pick<IssueProps, "message">>

Returns ​

this

Example ​

ts
import { pvl } from '@pcvl/schema';

const config = pvl.object({ port: pvl.number() }).strict();

config.validate({ port: 80, prot: 443 });
// { issues: [{ code: 'UNRECOGNIZED_KEY', path: ['prot'], ... }] }

transform() ​

ts
transform<NewOutput>(fn): Schema<Input, NewOutput>;

Defined in: schemas/chainableInterpretedSchema.ts:176

Converts an accepted value into a different one, changing the Output type to whatever the function returns. It is the last thing to run, and only once nothing has failed, so the function only ever sees a value this schema accepted — unlike .coerce(), which runs before the type check. A value .optional() or .nullable() accepted still reaches it, so the function is typed to receive undefined or null too.

It ends the chain: the result is a plain Schema, which can validate but takes no further modifier, so chain those before transforming. A transformed object has no shape, and a transformed array no element: they describe a value the function replaces.

Type Parameters ​

NewOutput ​

NewOutput

Parameters ​

fn ​

(value) => NewOutput

Returns ​

Schema<Input, NewOutput>

Example ​

ts
import { pvl } from '@pcvl/schema';

const trimmedLength = pvl.string().transform((value) => value.trim().length);

trimmedLength.validate('  hello  '); // { value: 5 }
trimmedLength.validate(42); // { issues: [...] } — never reaches the transform

const label = pvl.string().nullable().transform((value) => value ?? 'none');
label.validate(null); // { value: 'none' }

Inherited from ​

ChainableInterpretedSchema.transform


validate() ​

ts
validate(value): Result<Output>;

Defined in: schemas/interpretedSchema.ts:89

Checks a value against this schema and returns a Result synchronously. It never throws for an invalid value — a rejection comes back as issues, so the two branches are handled the same way every time.

Parameters ​

value ​

unknown

Returns ​

Result<Output>

Example ​

ts
import { pvl } from '@pcvl/schema';

const result = pvl.number().int().validate(1.5);
if (result.issues) {
  console.log(result.issues[0]?.message); // 'Number must be an integer'
} else {
  console.log(result.value);
}

Inherited from ​

ChainableInterpretedSchema.validate