JsonTology.is Compile-time + Runtime
Validation modes: Validation modes reference
Declaration. Validates data against a registered schema and returns a boolean. When the schema is registered via JsonTology.create({ schemas }), the return type is a TypeScript type predicate (data is ParseOutputType<TRefs[K], TRefs>), which narrows the type of data to the schema's inferred type inside the if block. Does not mutate input. Does not throw on validation failure.
Use this when you need a boolean check and you want TypeScript to narrow the type inside the truthy branch - for example, in union-narrowing guards, array filters, middleware checks. This is the idiomatic pattern when you need "is this data the right shape?" without wanting errors or a coerced value.
Don't use this when you need error details (use validate instead). Don't use it when you need the coerced, defaults-filled value (use instantiate instead). Invariants also run: is returns false when any registered invariant fails, not just when structural validation fails.
Examples
Example 1: Type narrowing in a conditional branch
/**
* is — Example 1: Type narrowing in a conditional branch
* Demonstrates: is() narrows data to Customer inside the if block
*
* Walter Moers as a valid customer; a plain string as an invalid one.
*/
import {
bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
function describeCustomer(data: unknown): string {
// Passing the schema $id selects the type-guard overload, narrowing `data`.
if (bookstoreEntities.is(CustomerSchema.$id, data)) {
// data is narrowed to Customer here
return `${String(data.name)} <${String(data.email)}>`;
}
return 'not a customer';
}
const valid = {
'addresses': [],
'customerId': 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'email': 'walter.moers@bookstore.example',
'name': 'Walter Moers'
};
console.assert(describeCustomer(valid) === 'Walter Moers <walter.moers@bookstore.example>');
console.assert(describeCustomer('not-a-customer') === 'not a customer');
console.assert(describeCustomer(null) === 'not a customer');
console.log('valid customer:', describeCustomer(valid));
console.log('invalid input:', describeCustomer('not-a-customer'));
Example 2: Filtering an array of unknowns
/**
* is — Example 2: Filtering an array of unknowns
* Demonstrates: type-predicate filter — result is Customer[]
*
* Mixed array of valid and invalid items; only the valid Customer objects pass.
*/
import {
bookstoreEntities, type Customer, CustomerSchema
} from '../bookstore/index.js';
// instantiate validates the raw input against the branded schema and returns
// the branded Customer value — a plain object literal lacks the format/length
// brands the type carries.
const validCustomer: Customer = bookstoreEntities.instantiate(CustomerSchema.$id, {
'addresses': [],
'customerId': 'b2c3d4e5-f6a7-4901-8def-012345678901',
'email': 'cornelia.funke@bookstore.example',
'name': 'Cornelia Funke'
});
const mixed: unknown[] = [
validCustomer,
{ 'email': 'not-a-customer' },
42,
null,
{
'addresses': [],
'customerId': 'c3d4e5f6-a7b8-4012-9efa-123456789012',
'email': 'patrick.suskind@bookstore.example',
'name': 'Patrick Süskind'
}
];
const customers = mixed.filter((item): item is Customer => {
return bookstoreEntities.is(CustomerSchema.$id, item);
});
// customers is Customer[]
console.assert(customers.length === 2);
const firstCustomer = customers[0];
const secondCustomer = customers[1];
if (firstCustomer === undefined || secondCustomer === undefined) {
throw new Error('expected two customers');
}
console.assert(firstCustomer.name === 'Cornelia Funke');
console.assert(secondCustomer.name === 'Patrick Süskind');
console.log('filtered customers:', customers.map((customer) => {
return customer.name;
}));
Example 3: Guards at a service boundary
/**
* is — Example 3: Guards at a service boundary
* Demonstrates: is() as a guard throwing TypeError on invalid shape
*
* processOrder rejects anything that isn't a valid Order; valid Bastian
* fixture passes through without an explicit cast.
*/
import {
aboxFixtures, bookstoreEntities, type Order, OrderSchema
} from '../bookstore/index.js';
function processOrder(data: unknown): string {
// Passing the schema $id selects the type-guard overload, narrowing `data`.
if (!bookstoreEntities.is(OrderSchema.$id, data)) {
throw new TypeError('Expected an Order');
}
// data is Order from here — no explicit cast needed
return `Processing order ${String(data.orderId)} for customer ${String(data.customerId)}`;
}
const validOrder: Order = bookstoreEntities.instantiate(
OrderSchema.$id,
aboxFixtures.order
);
const result = processOrder(validOrder);
console.assert(result.includes(aboxFixtures.order.orderId));
console.assert(result.includes(aboxFixtures.customer.customerId));
console.log(result);
let threw = false;
try {
processOrder({ 'id': 'not-an-order' });
} catch (error) {
threw = error instanceof TypeError;
console.log('invalid input threw TypeError:', threw);
}
console.assert(threw);
Bad examples - what NOT to do
Anti-pattern 1: Using is when you need the coerced (defaults-filled) value
/**
* is — Anti-pattern 1: Using is() when you need the coerced (defaults-filled) value
* Demonstrates: is() does not apply defaults; instantiate() does
*
* The Customer schema declares `addresses: { default: [] }`. A valid customer
* body without addresses passes is(), but the raw object won't have the
* default applied — only instantiate() fills it.
*/
import {
aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
const rawBody = {
'customerId': aboxFixtures.customer.customerId,
'email': aboxFixtures.customer.email,
'name': aboxFixtures.customer.name
// addresses omitted — schema default is []
};
// Anti-pattern: is() does not apply defaults
// Don't do this. Passing the schema $id narrows rawBody to Customer, where
// addresses is optional — but is() never fills the default, so it is undefined.
if (bookstoreEntities.is(CustomerSchema.$id, rawBody)) {
// rawBody.addresses is undefined here — default [] was never applied
// Calling rawBody.addresses.forEach(...) would throw at runtime
console.assert(rawBody.addresses === undefined || Array.isArray(rawBody.addresses));
console.log('is() passes, addresses after is():', rawBody.addresses);
}
// Correct approach: instantiate() to get defaults applied
const customer = bookstoreEntities.instantiate(CustomerSchema.$id, rawBody);
console.assert(Array.isArray(customer.addresses));
// addresses is always present after instantiate (default [])
console.log('addresses after instantiate():', customer.addresses);
Anti-pattern 2: Checking is and then immediately coercing
/**
* is — Anti-pattern 2: Checking is() and then immediately coercing
* Demonstrates: double validation (bad) vs direct instantiate with catch (correct)
*
* Bastian Balthazar Bux — valid fixture used to show the correct single-pass pattern.
*/
import { InstantiationError } from '../../../src/index.js';
import {
aboxFixtures, bookstoreEntities, CustomerSchema
} from '../bookstore/index.js';
// Anti-pattern: is() then instantiate() runs validation twice
// Don't do this
if (bookstoreEntities.is(CustomerSchema, aboxFixtures.customer)) {
// validates again — redundant
const _customer = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);
void _customer;
}
// Correct approach: instantiate directly; catch the error if invalid
try {
const customer = bookstoreEntities.instantiate(CustomerSchema, aboxFixtures.customer);
console.assert(customer.name === aboxFixtures.customer.name);
console.log('instantiate succeeded:', customer.name);
} catch (error) {
if (error instanceof InstantiationError) {
console.assert(false, 'Should not throw for valid fixture');
}
}
Comparison
if (jt.is(CustomerSchema.$id, data)) {
data.name; // typed as string - narrowed by is()
}const result = CustomerSchema.safeParse(data);
if (result.success) {
result.data.name; // typed via result.data - data itself is not narrowed
}
// Or write a wrapper type predicate:
function isCustomer(d: unknown): d is Customer {
return CustomerSchema.safeParse(d).success;
}import * as v from 'valibot';
if (v.is(CustomerSchema, data)) {
data.name; // narrowed to Customer
}// io-ts codecs expose `.is` as a type guard:
if (CustomerCodec.is(data)) {
data.name; // narrowed to t.TypeOf<typeof CustomerCodec>
}
// Note: .is checks runtime shape directly without producing decoded output.import { TypeCompiler } from '@sinclair/typebox/compiler';
const C = TypeCompiler.Compile(CustomerSchema);
if (C.Check(data)) {
data; // narrowed to Customer
}// ajv.validate returns boolean but doesn't narrow the TypeScript type.
// Write a type predicate wrapper:
function isCustomer(data: unknown): data is Customer {
return ajv.validate('Customer', data) as boolean;
}
if (isCustomer(data)) {
data.name; // typed
}# Python uses try/except rather than a boolean predicate:
try:
customer = Customer.model_validate(data)
# customer is typed as Customer
except ValidationError:
pass # not a valid customer// 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
JsonTology.validate- returns structuredValidationErrorswhen you need to display failuresJsonTology.instantiate- returns typed value with defaults applied- Invariants - cross-field rules that also affect
isreturn value
See also
- Type Inference - how the type predicate works with
TRefs - Bookstore domain - schema definitions used in examples