Free & open source — no account required
Free & open source — no account required
This technical guide provides an in-depth analysis of the json to arktype engine, best practices for implementation, and data security standards.
ArkType is built on a different premise than other validation libraries: your TypeScript types and your runtime validators should be the same thing, written in the same syntax. Instead of learning a parallel API, you write types using a syntax that is a superset of TypeScript's own type expressions — "string", "number", "string[]", "'admin' | 'user'" — and ArkType compiles them into a runtime validator at definition time. The result is schemas that are simultaneously the most concise, the most type-accurate, and among the fastest at runtime.
// Input JSON
{
"id": 101,
"username": "ark_dev",
"email": "contact@arktype.io",
"role": "admin",
"createdAt": "2024-01-15T08:30:00Z"
}
// Generated ArkType Schema
import { type } from 'arktype';
const User = type({
id: 'number.integer',
username: 'string > 3 & string < 50', // 3 < length < 50
email: 'string.email',
role: '"admin" | "user" | "guest"',
createdAt: 'string.date.iso',
});
// Validation — call the type as a function
const result = User(incomingData);
if (result instanceof type.errors) {
console.error(result.summary);
// "username must be more than 3 characters (was 2)"
} else {
// result is typed: { id: number; username: string; email: string; ... }
console.log(result.email);
}
// Type inference — exact TypeScript type
type UserType = typeof User.infer;
The string syntax "string > 3 & string < 50" is not magic strings — ArkType parses them as type expressions using the same precedence rules as TypeScript's own type system. Your IDE provides autocomplete and type errors inside those strings.
ArkType's scope() lets you register custom types that work like built-in primitives anywhere in your schema:
import { scope } from 'arktype';
const $ = scope({
// Register custom types
email: 'string.email',
uuid: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
positiveInt: 'number.integer & number > 0',
isoDate: 'string.date.iso',
// Compose them into schemas
user: {
id: 'uuid',
email: 'email',
balance: 'positiveInt',
joinedAt: 'isoDate',
},
// Reference user in another type
order: {
id: 'uuid',
userId: 'uuid',
total: 'number > 0',
'discount?': 'number >= 0 & number <= 100',
createdAt: 'isoDate',
}
});
// Export compiled types from the scope
const { user, order } = $.export();
type User = typeof user.infer;
type Order = typeof order.infer;
Custom types defined in a scope are resolved at definition time — "email" inside any schema in this scope maps to string.email, not a runtime lookup. The whole scope compiles to an optimized validator graph once.
ArkType's morph() validates input and transforms the output in a single composable step:
import { type } from 'arktype';
// Validate ISO string, return Date object
const DateField = type('string.date.iso').pipe(s => new Date(s));
// Validate string, return lowercase
const Email = type('string.email').pipe(s => s.toLowerCase());
// Validate number cents, return dollar amount
const Money = type('number.integer & number >= 0').pipe(cents => ({
cents,
dollars: cents / 100,
display: `$${(cents / 100).toFixed(2)}`,
}));
const UserSchema = type({
createdAt: DateField,
email: Email,
balance: Money,
});
const result = UserSchema({
createdAt: '2024-01-15T08:30:00Z',
email: 'USER@EXAMPLE.COM',
balance: 1999,
});
if (!(result instanceof type.errors)) {
result.createdAt; // Date object
result.email; // 'user@example.com'
result.balance.display; // '$19.99'
}
import { type } from 'arktype';
// Discriminated union using TypeScript union syntax
const Event = type({
type: '"click"',
x: 'number',
y: 'number',
} | {
type: '"keypress"',
key: 'string',
ctrl: 'boolean',
} | {
type: '"submit"',
formId: 'string',
payload: 'Record<string, string>',
});
type EventType = typeof Event.infer;
// ArkType uses the 'type' key as a discriminator automatically
// and routes validation to the matching branch — O(1) lookup
function handleEvent(raw: unknown) {
const event = Event(raw);
if (event instanceof type.errors) return;
if (event.type === 'click') {
console.log(event.x, event.y); // TypeScript knows these exist
}
}
ArkType compiles schemas to optimized JavaScript at definition time — not per validation call:
// This happens ONCE at module load:
const User = type({
id: 'number.integer',
email: 'string.email',
name: 'string > 0 & string <= 100',
});
// ArkType generates: roughly equivalent to:
// (v) => typeof v==='object'&&v!==null
// && Number.isInteger(v.id)
// && EMAIL_REGEX.test(v.email)
// && typeof v.name==='string'
// && v.name.length>0&&v.name.length<=100
// All subsequent validations are just the compiled function:
// User(data1) — hot function call, no schema traversal
// User(data2) — same
// User(data3) — same
Benchmarks consistently show ArkType validating 10-100× faster than Zod for complex schemas with many fields or nested objects. The gap is largest for high-frequency validation: real-time event processing, websocket message parsing, or API handlers under heavy load.
import { generic, type } from 'arktype';
// Define a generic paginated response type
const Paginated = generic(['T'])(
(args) => type({
items: args.T.array(),
total: 'number.integer',
page: 'number.integer & number > 0',
pageSize: 'number.integer & number > 0',
})
);
// Instantiate with a concrete type
const UserPage = Paginated(type({ id: 'string', name: 'string' }));
const ProductPage = Paginated(type({ sku: 'string', price: 'number > 0' }));
type UserPageType = typeof UserPage.infer;
type ProductPageType = typeof ProductPage.infer;
// tRPC + ArkType input validation
import { initTRPC } from '@trpc/server';
import { type } from 'arktype';
const t = initTRPC.create();
const CreateUserInput = type({
name: 'string > 0 & string <= 50',
email: 'string.email',
role: '"admin" | "user"',
});
export const appRouter = t.router({
createUser: t.procedure
.input((raw) => {
const result = CreateUserInput(raw);
if (result instanceof type.errors) throw result;
return result;
})
.mutation(async ({ input }) => {
// input is fully typed
return db.users.create({ data: input });
}),
});
// Fastify with ArkType validation
fastify.post('/users', {
handler: async (request) => {
const body = CreateUserInput(request.body);
if (body instanceof type.errors) {
return { status: 400, errors: body.summary };
}
return db.users.create({ data: body });
}
});
email, uuid, isoDate) creates a consistent vocabulary. New schemas in that scope automatically inherit and reuse them.instanceof type.errors: ArkType's result is either the validated value or a type.errors instance — use this discriminant rather than checking for undefined or null..pipe() for transformations after validation: Validate the input shape first, then transform — this separates the "is this valid?" logic from the "how do I represent this?" logic and makes schemas easier to test independently.Q: Does IDE autocomplete work inside ArkType's string syntax?
A: Yes. ArkType ships TypeScript type definitions that make string literal arguments template-literal-typed. In VS Code, typing "string." in an ArkType schema shows autocomplete for string.email, string.url, string.uuid, etc. Invalid type expressions show TypeScript errors at compile time.
Q: How does ArkType compare to Zod for everyday use?
A: Zod has a larger ecosystem (more integrations, more community examples) and a more familiar chained API. ArkType is more concise, faster at runtime, and more type-accurate for complex unions and intersections. For a new project where performance matters or schemas are complex, ArkType is worth learning. For a project already using Zod with existing integrations, the migration cost rarely justifies the switch.
Q: Can I use ArkType with React Hook Form?
A: Use @hookform/resolvers/arktype: resolver: arktypeResolver(YourSchema). The error format from ArkType maps to RHF's field error structure.
Q: Is the string syntax a runtime eval() risk?
A: No. ArkType parses the string syntax at definition time using a TypeScript parser, not eval(). The strings are analyzed statically — both at compile time (TypeScript checks them) and at schema definition time (ArkType compiles them to validator functions). No user-provided strings ever enter the type compilation pipeline at runtime.
Q: How do I handle optional fields?
A: Append ? to the key name: "fieldName?". This mirrors TypeScript's optional property syntax exactly — { "nickname?": "string" } produces { nickname?: string } in the inferred type.
Is the processing local-only?
Absolutely. TypeMorph operates entirely within your browser's sandbox. We use Web Workers for high-performance computation without ever transmitting your JSON, SQL, or API data to a remote server.
Can I use this for enterprise projects?
Yes. The tool is designed for professional software engineers who require GDPR compliance and data privacy. It is trusted by developers at top-tier startups and financial institutions.