Skip to content

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'));
Output
Press Execute to run this example against the real library.

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;
}));
Output
Press Execute to run this example against the real library.

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);
Output
Press Execute to run this example against the real library.

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);
Output
Press Execute to run this example against the real library.

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');
  }
}
Output
Press Execute to run this example against the real library.

Comparison

ts
if (jt.is(CustomerSchema.$id, data)) {
  data.name; // typed as string  - narrowed by is()
}
ts
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;
}
ts
import * as v from 'valibot';
if (v.is(CustomerSchema, data)) {
  data.name; // narrowed to Customer
}
ts
// 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.
ts
import { TypeCompiler } from '@sinclair/typebox/compiler';
const C = TypeCompiler.Compile(CustomerSchema);
if (C.Check(data)) {
  data; // narrowed to Customer
}
ts
// 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
}
py
# 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
ts
// Limitation: feature not directly supported in Yup. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Joi. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Effect Schema. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in ArkType. See /comparisons for the matrix.
ts
// Limitation: feature not directly supported in Runtypes. See /comparisons for the matrix.

See also

Released under the MIT License.