Compose.pick and Compose.omit
Validation modes: Validation modes reference
pick and omit are inverse operations for creating schema projections. Both return new schema objects - input schemas are never mutated.
Compose.pick {#compose-pick} Compile-time + Runtime
Declaration. Creates a new schema containing only the specified property keys. The required array is filtered to include only keys that were in the original required and appear in the keys argument. Non-picked required fields are dropped. TypeScript infers a type with only the picked properties.
Use this when you need a schema that exposes only a subset of fields - for API projections, list-view summaries, or public interfaces that should not expose all internal fields. This is the runtime equivalent of TypeScript's Pick<T, K>.
Don't use this when you need to remove specific fields while keeping the rest (use omit). Don't use it when you want to add fields (use extend).
Examples
Example 1: Book catalog summary - only display fields
/**
* Compose.pick / omit — Example 1: BookSummary and PublicBook
* Demonstrates: pick keeps fields, omit removes fields, required adjusted
*
* Book is now a Compose.subClassOf(BibliographicRecordSchema, …) — it is an
* allOf composition, so `Compose.pick` sees only Book's OWN (retail) keys:
* annotations, inStock, price, printStatus, ratings, stockLevel. Bibliographic
* fields (isbn, title, authors, publishedOn) live on BibliographicRecordSchema.
*
* Pick the bibliographic summary from BibliographicRecordSchema; omit the
* inventory field from BookSchema (retail view). Both lessons remain intact.
*
* Derived schemas register onto the canonical bookstore via
* `jt.set()`. Every validate/instantiate call goes
* through the same registry the rest of the docs reference, using
* the canonical Bastian-orders-Neverending-Story fixture data.
*/
import { Compose } from '../../../src/index.js';
import {
aboxFixtures, BibliographicRecordSchema, BookSchema,
createBookstoreDocRegistry
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
// pick — select only the bibliographic identity fields from BibliographicRecordSchema.
// isbn and title are NOT on BookSchema's own properties (they live on the base),
// so we target the base schema directly.
const BookSummarySchema = Compose.pick(
BibliographicRecordSchema,
[
'isbn',
'title'
] as const,
'https://bookstore.example/BookSummary'
);
// omit — derive a public-facing Book view by removing the operational inventory field.
const PublicBookSchema = Compose.omit(
BookSchema,
['inStock'] as const,
'https://bookstore.example/PublicBook'
);
const jt2 = jt.set(BookSummarySchema).set(PublicBookSchema);
// BookSummary — only picked fields survive
const summary = jt2.instantiate(BookSummarySchema, {
'isbn': aboxFixtures.rareBook.isbn,
'title': aboxFixtures.rareBook.title
});
console.assert(!('authors' in summary));
console.assert(summary.isbn === aboxFixtures.rareBook.isbn);
console.log('BookSummary picked keys:', Object.keys(summary));
// PublicBook — inStock removed, printStatus still required
const pub = jt2.validate(PublicBookSchema.$id, {
'authors': aboxFixtures.rareBook.authors,
'isbn': aboxFixtures.rareBook.isbn,
'price': aboxFixtures.rareBook.price,
'printStatus': aboxFixtures.rareBook.printStatus,
'title': aboxFixtures.rareBook.title
});
console.assert(pub.length === 0);
console.log('PublicBook valid without inStock:', pub.length === 0, '| omitted inStock from properties');
Example 2: Customer card for embedding in order responses
/**
* Compose.pick — Example 2: Customer card for embedding in order responses
*
* Picks only display fields off the canonical CustomerSchema to embed
* inside Order responses without leaking the customer's full address
* book.
*/
import { Compose } from '../../../src/index.js';
import {
aboxFixtures, createBookstoreDocRegistry,
CustomerSchema
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
const CustomerCardSchema = Compose.pick(
CustomerSchema,
[
'customerId',
'name',
'email'
] as const,
'https://bookstore.example/CustomerCard'
);
const jt2 = jt.set(CustomerCardSchema);
const card = jt2.instantiate(CustomerCardSchema.$id, {
'customerId': aboxFixtures.customer.customerId,
'email': aboxFixtures.customer.email,
'name': aboxFixtures.customer.name
}) as Record<string, unknown>;
console.assert(card.customerId === aboxFixtures.customer.customerId);
console.assert(card.name === 'Bastian Balthazar Bux');
console.assert(!('addresses' in card));
console.log('CustomerCard picked fields:', Object.keys(card), '| addresses omitted:', !('addresses' in card));
Example 3: Build sub-schema for partial validation (builds on subschemaAt)
/**
* Compose.pick — Example 3: Single-field sub-schema for blur validation
*
* Pick a single field from the canonical ReviewSchema and register it
* onto the bookstore. The derived schema validates the rating slot in
* isolation — useful for incremental field-level form validation.
*/
import { Compose } from '../../../src/index.js';
import {
createBookstoreDocRegistry,
ReviewSchema
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
const ReviewRatingSchema = Compose.pick(
ReviewSchema,
['rating'] as const,
'https://bookstore.example/ReviewRating'
);
const jt2 = jt.set(ReviewRatingSchema);
// A 5 passes — within 0..5.
const okResult = jt2.validate(ReviewRatingSchema.$id, { 'rating': 5 });
console.assert(okResult.ok);
console.log('ReviewRating validates rating=5:', okResult.ok);
// A 6 fails — exceeds the canonical rating cap.
const overResult = jt2.validate(ReviewRatingSchema.$id, { 'rating': 6 });
console.assert(!overResult.ok);
console.log('ReviewRating rejects rating=6:', !overResult.ok, '(exceeds max 5)');
Argument validation Compile-time
keys are bound to keyof properties. Passing a key that does not exist in the source schema's properties is a compile-time error rather than a silent empty-properties result.
/**
* Anti-pattern: calling `Compose.pick` with a key that does not exist in
* the source schema's `properties`. The compiler narrows `keys` to
* `keyof properties`, so the unknown key is rejected at the call site.
*
* Book is now a Compose.subClassOf(BibliographicRecordSchema, …) — it is an
* allOf composition without a flat top-level `properties` object. Picking
* from a composed schema would bypass the key-narrowing guard. We target
* BibliographicRecordSchema directly: it has a concrete `properties` object,
* so TypeScript can narrow `keys` to its known property names and reject any
* unknown key at the call site. The anti-pattern is identical; only the
* source schema changes to one with top-level properties.
*/
import { Compose } from '../../../src/index.js';
import { BibliographicRecordSchema } from '../bookstore/index.js';
// ✗ Compile error — 'nonExistent' is not a key of BibliographicRecordSchema.properties.
const _Bad = Compose.pick(
BibliographicRecordSchema,
[
'isbn',
// @ts-expect-error 'nonExistent' is not a key of BibliographicRecordSchema.properties
'nonExistent'
] as const,
'https://bookstore.example/BookIsbnOnly'
);
console.log('pick unknown key anti-pattern: compile-time error for keys not in BibliographicRecordSchema.properties | valid keys:', Object.keys(BibliographicRecordSchema.properties), '| _Bad.$id:', _Bad.$id);
Bad examples - what NOT to do
Anti-pattern 1: Forgetting as const on the keys array
/**
* Compose.pick — Anti-pattern 1: Forgetting `as const` on the keys array
*
* Without `as const`, the keys array widens to `string[]` and
* TypeScript loses the literal types — pick can no longer narrow the
* inferred property set. Always pass keys as a `const` tuple.
*
* Book is now a Compose.subClassOf(BibliographicRecordSchema, …) — isbn and
* title are bibliographic fields that live on BibliographicRecordSchema, not
* on Book's own properties. We target BibliographicRecordSchema directly so
* the keys are valid; the `as const` lesson is unchanged.
*/
import { Compose } from '../../../src/index.js';
import { BibliographicRecordSchema } from '../bookstore/index.js';
// ✓ Do this — `as const` preserves literal types so pick narrows correctly.
const BookSummarySchema = Compose.pick(
BibliographicRecordSchema,
[
'isbn',
'title'
] as const,
'https://bookstore.example/BookSummaryConst'
);
const summaryId: string = BookSummarySchema.$id;
console.assert(summaryId.endsWith('BookSummaryConst'));
console.log('pick with as const preserves literal key types:', Object.keys(BookSummarySchema.properties));
Comparison
Compose.pick(BookSchema, ['isbn', 'title', 'price'] as const, 'https://bookstore.example/BookSummary')BookSchema.pick({ isbn: true, title: true, price: true })import * as v from 'valibot';
v.pick(BookSchema, ['isbn', 'title', 'price'])import * as t from 'io-ts';
// Limitation: io-ts has no built-in pick. Reconstruct the codec from the
// fields you want, or use t.intersection of a subset codec.
const BookSummary = t.type({
isbn: BookCodec.props.isbn,
title: BookCodec.props.title,
price: BookCodec.props.price,
});import { Type } from '@sinclair/typebox';
// TypeBox has Type.Pick:
Type.Pick(BookSchema, ['isbn', 'title', 'price'])// Manual construction:
const BookSummary = {
$id: 'BookSummary',
type: 'object',
properties: { isbn: BookSchema.properties.isbn, title: BookSchema.properties.title, price: BookSchema.properties.price },
required: BookSchema.required?.filter(k => ['isbn', 'title', 'price'].includes(k)),
};book.model_dump(include={'isbn', 'title', 'price'})
# Or define a separate model class:
class BookSummary(BaseModel):
isbn: str
title: str
price: float// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.Related
omit- inverse: keep everything except specified keysextend- add new propertiespartial- make all properties optional
Compose.omit {#compose-omit} Compile-time + Runtime
Declaration. Creates a new schema with the specified property keys removed from properties. Removed keys are also dropped from required. TypeScript infers a type without the omitted properties.
Use this when you need to remove specific fields while keeping the rest - for example, stripping currency from Book for a region-normalized API, or removing addresses from Customer for a public profile endpoint. This is the runtime equivalent of TypeScript's Omit<T, K>.
Don't use this when you need to keep only specific fields (use pick). Don't use it when you want to add fields (use extend).
Examples
Example 1: Public book without internal currency field
/**
* Compose.omit — Example 1: Public book without the internal printStatus
*
* Drops a single field from BookSchema for a region-normalised public
* feed. The omitted field is removed from both `properties` and
* `required`; everything else carries through.
*/
import { Compose } from '../../../src/index.js';
import {
aboxFixtures, BookSchema,
createBookstoreDocRegistry
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
const PublicBookSchema = Compose.omit(
BookSchema,
['printStatus'] as const,
'https://bookstore.example/PublicBookNoStatus'
);
const jt2 = jt.set(PublicBookSchema);
const errors = jt2.validate(PublicBookSchema.$id, {
'authors': aboxFixtures.rareBook.authors,
'inStock': aboxFixtures.rareBook.inStock,
'isbn': aboxFixtures.rareBook.isbn,
'price': aboxFixtures.rareBook.price,
'title': aboxFixtures.rareBook.title
// printStatus omitted — schema does not require it
});
console.assert(errors.ok);
console.log('PublicBookNoStatus validates without printStatus:', errors.ok, '| omitted fields:', ['printStatus']);
Example 2: Order summary without line items
/**
* Compose.omit — Example 2: Order summary without line items
*
* Drops the `orderLines` array from the canonical OrderSchema to produce a
* compact summary suitable for dashboard rows. The derived type still
* carries `orderId`, `customerId`, `orderTotal`, `placedAt`, and the shipping
* address.
*/
import { Compose } from '../../../src/index.js';
import type { InferType } from '../../../src/types/index.js';
import {
aboxFixtures, createBookstoreDocRegistry,
OrderSchema
} from '../bookstore/index.js';
import type { BookstoreRefs } from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
const OrderSummarySchema = Compose.omit(
OrderSchema,
['orderLines'] as const,
'https://bookstore.example/OrderSummary'
);
type OrderSummary = InferType<typeof OrderSummarySchema, BookstoreRefs>;
const jt2 = jt.set(OrderSummarySchema);
// Instantiate the full order to get branded field values, then project the
// OrderSummary view (every field except orderLines) from them.
const order = jt2.instantiate(OrderSchema.$id, aboxFixtures.order);
const summary: OrderSummary = {
'customerId': order.customerId,
'orderId': order.orderId,
'orderTotal': order.orderTotal,
'placedAt': order.placedAt,
'shippingAddress': order.shippingAddress
};
const result = jt2.validate(OrderSummarySchema.$id, summary);
console.assert(result.ok);
console.assert(!('orderLines' in summary));
console.log('OrderSummary fields:', Object.keys(summary), '| orderLines omitted:', !('orderLines' in summary));
Example 3: Build a derived schema from a retrieved schema (builds on get)
/**
* Compose.omit — Example 3: Derive a schema from one already in the registry
*
* `jt.registry.get(id)` retrieves a previously
* registered schema. We narrow it with `Compose.omit` and register
* the derivation back onto the same registry.
*/
import { Compose } from '../../../src/index.js';
import {
BookSchema,
createBookstoreDocRegistry
} from '../bookstore/index.js';
// createBookstoreDocRegistry seeds a permissive copy of the bookstore — docs examples extend
// it with ad-hoc demo schemas; strict-graph checking is intentionally off here.
const jt = createBookstoreDocRegistry();
const retrieved = jt.registry.get(BookSchema.$id);
if (retrieved !== undefined) {
const BookWithoutStockSchema = Compose.omit(
retrieved as typeof BookSchema,
['inStock'] as const,
'https://bookstore.example/BookWithoutStock'
);
const jt2 = jt.set(BookWithoutStockSchema);
const result = jt2.validate(BookWithoutStockSchema.$id, {
'authors': ['Michael Ende'],
'isbn': '9783522115056',
'price': {
'amount': 16.99,
'currency': 'EUR'
},
'printStatus': 'inPrint',
'title': 'Momo'
});
console.assert(result.ok);
console.log('BookWithoutStock derived from registry and validates without inStock:', result.ok);
}
Argument validation Compile-time
keys are bound to keyof properties, the same constraint pick uses. Passing a key that does not exist in the source schema's properties is a compile-time error rather than a silent no-op.
Comparison
Compose.omit(CustomerSchema, ['addresses'] as const, 'https://bookstore.example/CustomerPublic')CustomerSchema.omit({ addresses: true })import * as v from 'valibot';
v.omit(CustomerSchema, ['addresses'])import * as t from 'io-ts';
// Limitation: io-ts has no built-in omit. Rebuild the codec from the fields
// you want to keep.
const { addresses: _drop, ...rest } = CustomerCodec.props;
const CustomerPublic = t.type(rest);import { Type } from '@sinclair/typebox';
Type.Omit(CustomerSchema, ['addresses'])// Manual - copy schema, delete key from properties and required:
const { addresses: _, ...props } = CustomerSchema.properties;
const req = CustomerSchema.required?.filter(k => k !== 'addresses') ?? [];
const CustomerPublic = { ...CustomerSchema, properties: props, required: req };customer.model_dump(exclude={'addresses'})
# Or define a derived model:
class CustomerPublic(BaseModel):
id: str
email: str
name: str// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.Related
pick- keep only specified fieldspartial- make remaining fields optional after omitextend- add new properties
See also
- Bookstore domain - where
BookSchema,CustomerSchema,OrderSchemaare defined - Composition index - overview of all composition operations