{"_id":"@remix-run/data-schema","_rev":"6-b4fc0010b22de90ccc3d37a7b11b20a4","name":"@remix-run/data-schema","dist-tags":{"latest":"0.3.1"},"versions":{"0.0.0":{"name":"@remix-run/data-schema","version":"0.0.0","keywords":["remix","placeholder"],"license":"MIT","_id":"@remix-run/data-schema@0.0.0","maintainers":[{"name":"mjackson","email":"mjijackson+npm@gmail.com"},{"name":"ryanflorence","email":"rpflorence@gmail.com"}],"homepage":"https://github.com/remix-run/remix/tree/main/packages/data-schema#readme","bugs":{"url":"https://github.com/remix-run/remix/issues"},"dist":{"shasum":"ec2b21e462abdcd6723080dee710af241555d1ce","tarball":"https://registry.npmjs.org/@remix-run/data-schema/-/data-schema-0.0.0.tgz","fileCount":2,"integrity":"sha512-/pNTgYKiF86ioPRY47mFHSD9vm4L7z0nD1sqeRv2PUuwT5CLL+NkCwS+Sa/MxTYeZ1fFJ0a7QtYS6n6visJiUA==","signatures":[{"sig":"MEUCIHM7OAOuBI8AElKYx1+1FEsBTtncdUhR+xq8dPLpYE5yAiEA3CtBKGIzdm1Rq21XKnwNdeep0qOnQp09I9pI03tnkpo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":611},"gitHead":"ef934f523767154caef1b3ab3c4e536f2e390141","private":false,"_npmUser":{"name":"mjackson","email":"mjijackson+npm@gmail.com"},"repository":{"url":"git+https://github.com/remix-run/remix.git","type":"git","directory":"packages/data-schema"},"_npmVersion":"11.8.0","description":"Placeholder package for future Remix data-schema APIs","directories":{},"_nodeVersion":"24.2.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/data-schema_0.0.0_1770854142582_0.8779474477836222","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"@remix-run/data-schema","version":"0.1.0","keywords":["validation","schema","standard-schema","validator","remix"],"author":{"name":"Michael Jackson","email":"mjijackson@gmail.com"},"license":"MIT","_id":"@remix-run/data-schema@0.1.0","maintainers":[{"name":"mjackson","email":"mjijackson+npm@gmail.com"}],"homepage":"https://github.com/remix-run/remix/tree/main/packages/data-schema#readme","bugs":{"url":"https://github.com/remix-run/remix/issues"},"dist":{"shasum":"17507358a058295a5001ee99c70692b9080fc574","tarball":"https://registry.npmjs.org/@remix-run/data-schema/-/data-schema-0.1.0.tgz","fileCount":35,"integrity":"sha512-PzpYP9P19cb8bS7Y9+MSxyAWGy0n13sx1lYOMoKI+iEx7pdB2ZLaiidwcn1l6AeR6gjVIpuzyM6/UONG3bzboA==","signatures":[{"sig":"MEQCIFCNwKZpOH1MAOOSkYlATCyD4erbV1JagqKdTNE4CFYxAiAPz45ZGYT7Ef7f6IHtl8ZshEli60QCMR5giS1fce+qmQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@remix-run%2fdata-schema@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":114084},"type":"module","_from":"file:remix-run-data-schema-0.1.0.tgz","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./lazy":{"types":"./dist/lazy.d.ts","default":"./dist/lazy.js"},"./checks":{"types":"./dist/checks.d.ts","default":"./dist/checks.js"},"./coerce":{"types":"./dist/coerce.d.ts","default":"./dist/coerce.js"},"./package.json":"./package.json"},"scripts":{"test":"node --disable-warning=ExperimentalWarning --test","build":"tsgo -p tsconfig.build.json","clean":"git clean -fdX","typecheck":"tsgo --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:04606dc2-2a71-4d11-9f6c-f5c4884dd7ca"}},"_resolved":"/tmp/681db020d210ac4e39933f4daf8fce87/remix-run-data-schema-0.1.0.tgz","_integrity":"sha512-PzpYP9P19cb8bS7Y9+MSxyAWGy0n13sx1lYOMoKI+iEx7pdB2ZLaiidwcn1l6AeR6gjVIpuzyM6/UONG3bzboA==","repository":{"url":"git+https://github.com/remix-run/remix.git","type":"git","directory":"packages/data-schema"},"_npmVersion":"11.10.1","description":"Tiny, standards-aligned schema validation","directories":{},"sideEffects":false,"_nodeVersion":"25.7.0","dependencies":{"@standard-schema/spec":"^1.1.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^24.6.0","@typescript/native-preview":"7.0.0-dev.20251125.1"},"_npmOperationalInternal":{"tmp":"tmp/data-schema_0.1.0_1772239261570_0.057681500705630384","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@remix-run/data-schema","version":"0.2.0","keywords":["validation","schema","standard-schema","validator","remix"],"author":{"name":"Michael Jackson","email":"mjijackson@gmail.com"},"license":"MIT","_id":"@remix-run/data-schema@0.2.0","maintainers":[{"name":"mjackson","email":"mjijackson+npm@gmail.com"}],"homepage":"https://github.com/remix-run/remix/tree/main/packages/data-schema#readme","bugs":{"url":"https://github.com/remix-run/remix/issues"},"dist":{"shasum":"21090dfefa05c5a8d07be81e5f4c262c06dc874d","tarball":"https://registry.npmjs.org/@remix-run/data-schema/-/data-schema-0.2.0.tgz","fileCount":43,"integrity":"sha512-bXgdWcWZS6TkbVO9Piwn035iw0LczfSTFrCW4Vd1k57x/4j3eWZwfHBr3lXdw5whefacs3m3pVZ8RUiu8NtBeA==","signatures":[{"sig":"MEUCIQCmf1Jdo6pE7zTiPWPCRHZicKD/ILsRdk1NpS9yiR4YGwIgD48EARyMAi+7XGmEW+7xwRKdAmd3/vW4ZRHOyiyx58g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@remix-run%2fdata-schema@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":141055},"type":"module","_from":"file:remix-run-data-schema-0.2.0.tgz","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./lazy":{"types":"./dist/lazy.d.ts","default":"./dist/lazy.js"},"./checks":{"types":"./dist/checks.d.ts","default":"./dist/checks.js"},"./coerce":{"types":"./dist/coerce.d.ts","default":"./dist/coerce.js"},"./form-data":{"types":"./dist/form-data.d.ts","default":"./dist/form-data.js"},"./package.json":"./package.json"},"scripts":{"test":"node --test","build":"tsgo -p tsconfig.build.json","clean":"git clean -fdX","typecheck":"tsgo --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:04606dc2-2a71-4d11-9f6c-f5c4884dd7ca"}},"_resolved":"/tmp/664ab6fb3a89d45f7cfd8b6fd63313fd/remix-run-data-schema-0.2.0.tgz","_integrity":"sha512-bXgdWcWZS6TkbVO9Piwn035iw0LczfSTFrCW4Vd1k57x/4j3eWZwfHBr3lXdw5whefacs3m3pVZ8RUiu8NtBeA==","repository":{"url":"git+https://github.com/remix-run/remix.git","type":"git","directory":"packages/data-schema"},"_npmVersion":"11.9.0","description":"Tiny, standards-aligned schema validation","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"@standard-schema/spec":"^1.1.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^24.6.0","@typescript/native-preview":"7.0.0-dev.20251125.1"},"_npmOperationalInternal":{"tmp":"tmp/data-schema_0.2.0_1774479801782_0.9397920866653298","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@remix-run/data-schema","version":"0.3.0","keywords":["validation","schema","standard-schema","validator","remix"],"author":{"name":"Michael Jackson","email":"mjijackson@gmail.com"},"license":"MIT","_id":"@remix-run/data-schema@0.3.0","maintainers":[{"name":"mjackson","email":"mjijackson+npm@gmail.com"}],"homepage":"https://github.com/remix-run/remix/tree/main/packages/data-schema#readme","bugs":{"url":"https://github.com/remix-run/remix/issues"},"dist":{"shasum":"0660f325ba717a3214a68a0ef9c4aaaa05b2c203","tarball":"https://registry.npmjs.org/@remix-run/data-schema/-/data-schema-0.3.0.tgz","fileCount":43,"integrity":"sha512-rjGaFJduzO3iMFOKwA5URpZDuGbUxgBwcX9myBglfqbax8dBlhRcxurydepq+xi+XBE+bPrT9V57Jur6p1igow==","signatures":[{"sig":"MEYCIQDFZ9gwPX6HOxBe9BpV1EH60LlgKq4kV9OUs0H5lsorWAIhAOAK6P43tsxbG5dysAxvs4JpBgtRhuhkKhxbjmaawxsn","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@remix-run%2fdata-schema@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":143696},"type":"module","_from":"file:remix-run-data-schema-0.3.0.tgz","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./lazy":{"types":"./dist/lazy.d.ts","default":"./dist/lazy.js"},"./checks":{"types":"./dist/checks.d.ts","default":"./dist/checks.js"},"./coerce":{"types":"./dist/coerce.d.ts","default":"./dist/coerce.js"},"./form-data":{"types":"./dist/form-data.d.ts","default":"./dist/form-data.js"},"./package.json":"./package.json"},"scripts":{"test":"remix-test","build":"tsgo -p tsconfig.build.json","clean":"git clean -fdX","test:bun":"bun x --bun remix-test","typecheck":"tsgo --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:04606dc2-2a71-4d11-9f6c-f5c4884dd7ca"}},"_resolved":"/tmp/37f256ccc4ea0cc91bfb45e0baff233f/remix-run-data-schema-0.3.0.tgz","_integrity":"sha512-rjGaFJduzO3iMFOKwA5URpZDuGbUxgBwcX9myBglfqbax8dBlhRcxurydepq+xi+XBE+bPrT9V57Jur6p1igow==","repository":{"url":"git+https://github.com/remix-run/remix.git","type":"git","directory":"packages/data-schema"},"_npmVersion":"11.11.0","description":"Tiny, standards-aligned schema validation","directories":{},"sideEffects":false,"_nodeVersion":"24.14.1","dependencies":{"@standard-schema/spec":"^1.1.0"},"_hasShrinkwrap":false,"devDependencies":{"@types/node":"^24.6.0","@remix-run/test":"0.2.0","@remix-run/assert":"0.1.0","@typescript/native-preview":"7.0.0-dev.20251125.1"},"_npmOperationalInternal":{"tmp":"tmp/data-schema_0.3.0_1777483766005_0.6068819335200364","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"_id":"@remix-run/data-schema@0.3.1","bugs":{"url":"https://github.com/remix-run/remix/issues"},"dist":{"shasum":"87ff16073ed19fa31bb498cb1f26c7f91ad77238","tarball":"https://registry.npmjs.org/@remix-run/data-schema/-/data-schema-0.3.1.tgz","fileCount":43,"integrity":"sha512-Gr3Idi5e0L3zULGXpShq3RCcwbvKq1paSN2jhPd7zI4Z8H64a4SAwr3xyEV1z+jbT6EeCnSRB+dZDHN4wuC42w==","signatures":[{"sig":"MEUCIHA2+uh4C6BrhfXrhLUM7/5H14jJ5oFt155bSTLNDqteAiEAvYcHDxnQwL4JDMDPCZl7q4rpiErwz81V4yqoB2731b0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGGKHhDW0eCZ/J6A2kKwG44iZo/5HGJTvjXgT14l7sBqAiEAkCAewhX849LGa9/DfNitgE+r416uvZfOP77fKER5iNk="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@remix-run%2fdata-schema@0.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":145769},"name":"@remix-run/data-schema","type":"module","_from":"file:/tmp/remix-publish-data-schema-L4O9uc/remix-run-data-schema-0.3.1.tgz","author":{"name":"Michael Jackson","email":"mjijackson@gmail.com"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./lazy":{"types":"./dist/lazy.d.ts","default":"./dist/lazy.js"},"./checks":{"types":"./dist/checks.d.ts","default":"./dist/checks.js"},"./coerce":{"types":"./dist/coerce.d.ts","default":"./dist/coerce.js"},"./form-data":{"types":"./dist/form-data.d.ts","default":"./dist/form-data.js"},"./package.json":"./package.json"},"license":"MIT","scripts":{"test":"remix test","build":"tsc -p tsconfig.build.json","clean":"git clean -fdX","test:bun":"bun x --bun remix test","typecheck":"tsc --noEmit"},"version":"0.3.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:04606dc2-2a71-4d11-9f6c-f5c4884dd7ca"}},"homepage":"https://github.com/remix-run/remix/tree/main/packages/data-schema#readme","keywords":["validation","schema","standard-schema","validator","remix"],"_resolved":"/tmp/remix-publish-data-schema-L4O9uc/remix-run-data-schema-0.3.1.tgz","_integrity":"sha512-Gr3Idi5e0L3zULGXpShq3RCcwbvKq1paSN2jhPd7zI4Z8H64a4SAwr3xyEV1z+jbT6EeCnSRB+dZDHN4wuC42w==","repository":{"url":"git+https://github.com/remix-run/remix.git","type":"git","directory":"packages/data-schema"},"_npmVersion":"11.19.0","description":"Tiny, standards-aligned schema validation","directories":{},"maintainers":[{"name":"mjackson","email":"mjijackson+npm@gmail.com"}],"sideEffects":false,"_nodeVersion":"24.20.0","dependencies":{"@standard-schema/spec":"^1.1.0"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^7.0.2","@types/node":"^24.6.0","@remix-run/test":"0.6.1","@remix-run/assert":"0.3.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/data-schema_0.3.1_1789770888639_0.3112075090556694"}}},"time":{"created":"2026-02-11T23:55:42.418Z","modified":"2026-09-18T22:34:49.039Z","0.0.0":"2026-02-11T23:55:42.746Z","0.1.0":"2026-02-28T00:41:01.701Z","0.2.0":"2026-03-25T23:03:21.933Z","0.3.0":"2026-04-29T17:29:26.137Z","0.3.1":"2026-09-18T22:34:48.724Z"},"bugs":{"url":"https://github.com/remix-run/remix/issues"},"author":{"name":"Michael Jackson","email":"mjijackson@gmail.com"},"license":"MIT","homepage":"https://github.com/remix-run/remix/tree/main/packages/data-schema#readme","keywords":["validation","schema","standard-schema","validator","remix"],"repository":{"url":"git+https://github.com/remix-run/remix.git","type":"git","directory":"packages/data-schema"},"description":"Tiny, standards-aligned schema validation","maintainers":[{"name":"mjackson","email":"mjijackson+npm@gmail.com"}],"readme":"# data-schema\n\nTiny, standards-aligned data validation for Remix and the wider TypeScript ecosystem.\n\n- [Standard Schema](https://standardschema.dev/) v1 compatible\n- Sync-first, minimal API surface\n- Runs anywhere JavaScript runs (browser, Node.js, Bun, Deno, Workers)\n\n## Quick start\n\n```ts\nimport { enum_, literal, number, object, parse, string, variant } from 'remix/data-schema'\nimport { email, maxLength, min, minLength } from 'remix/data-schema/checks'\nimport * as coerce from 'remix/data-schema/coerce'\n\nlet User = object({\n  id: string(),\n  email: string().pipe(email()),\n  username: string().pipe(minLength(3), maxLength(20)),\n  age: coerce.number().pipe(min(13)),\n  role: enum_(['admin', 'member', 'guest']),\n  flags: object({\n    beta: coerce.boolean(),\n  }),\n})\n\nlet Event = variant('type', {\n  created: object({ type: literal('created'), id: string() }),\n  updated: object({ type: literal('updated'), id: string(), version: number() }),\n})\n\nlet user = parse(User, {\n  id: 'u1',\n  email: 'ada@example.com',\n  username: 'ada',\n  age: '37',\n  role: 'admin',\n  flags: { beta: 'true' },\n})\n\nlet event = parse(Event, { type: 'created', id: 'evt_1' })\n```\n\n## Parsing\n\nUse `parse()` when you want a typed value or an exception.\n\n```ts\nimport { object, string, number, parse } from 'remix/data-schema'\n\nlet User = object({ name: string(), age: number() })\n\nlet user = parse(User, { name: 'Ada', age: 37 })\n```\n\nUse `parseSafe()` when you prefer explicit branching over exceptions.\n\n```ts\nimport { object, string, number, parseSafe } from 'remix/data-schema'\n\nlet User = object({ name: string(), age: number() })\n\nlet result = parseSafe(User, input)\n\nif (!result.success) {\n  // result.issues — array of { message, path? }\n} else {\n  let user = result.value\n}\n```\n\nBoth `parse` and `parseSafe` accept any [Standard Schema](https://standardschema.dev/) v1 schema, not just data-schema's own schemas. You can pass a Zod, Valibot, or ArkType schema and they'll work.\n\nFor `FormData` and `URLSearchParams`, use the `remix/data-schema/form-data` helpers to build schemas that plug into the same `parse()` / `parseSafe()` flow:\n\n```ts\nimport * as s from 'remix/data-schema'\nimport * as f from 'remix/data-schema/form-data'\nimport * as checks from 'remix/data-schema/checks'\nimport * as coerce from 'remix/data-schema/coerce'\n\nlet Login = f.object({\n  email: f.field(coerce.string().pipe(checks.email())),\n  password: f.field(s.string().pipe(checks.minLength(8))),\n})\n\nlet credentials = s.parse(Login, await request.formData())\nlet filters = s.parse(\n  f.object({\n    query: f.field(s.defaulted(s.string(), '')),\n    tags: f.fields(s.array(s.string())),\n  }),\n  new URL(request.url).searchParams,\n)\n```\n\n`f.object(...)` is the root schema for `FormData` and `URLSearchParams`. Use `f.field(...)` for one text value, `f.fields(...)` for repeated text values, `f.file(...)` for one uploaded file, and `f.files(...)` for repeated files. When you want a fallback value, prefer `s.defaulted(s.string(), '')`. File helpers are intended for `FormData`; `URLSearchParams` only supports text values.\n\nYou can also customize built-in validation messages with `errorMap`:\n\n```ts\nimport { object, parseSafe, string } from 'remix/data-schema'\nimport { minLength } from 'remix/data-schema/checks'\n\nlet User = object({\n  name: string(),\n  username: string().pipe(minLength(3)),\n})\nlet result = parseSafe(User, input, {\n  locale: 'es',\n  errorMap(context) {\n    if (context.code === 'type.string') {\n      return 'Se esperaba texto'\n    }\n\n    if (context.code === 'string.min_length') {\n      return (\n        'Debe tener al menos ' + String((context.values as { min: number }).min) + ' caracteres'\n      )\n    }\n  },\n})\n```\n\n`errorMap` receives `{ code, defaultMessage, path, values, input, locale }`. Return `undefined` to keep the default message.\n\n## Primitives\n\n```ts\nimport { string, number, boolean, bigint, symbol, null_, undefined_ } from 'remix/data-schema'\n\nstring() // validates typeof === 'string'\nnumber() // validates finite numbers (rejects NaN, Infinity)\nboolean() // validates typeof === 'boolean'\nbigint() // validates typeof === 'bigint'\nsymbol() // validates typeof === 'symbol'\nnull_() // validates value === null\nundefined_() // validates value === undefined\n```\n\n## Literals, enums, and unions\n\n```ts\nimport { literal, enum_, union } from 'remix/data-schema'\n\n// Exact value match\nlet yes = literal('yes')\n\n// One of several allowed values\nlet Status = enum_(['active', 'inactive', 'pending'])\n\n// First schema that matches wins\nlet StringOrNumber = union([string(), number()])\n```\n\n## Objects\n\n```ts\nimport { object, string, number, optional, defaulted } from 'remix/data-schema'\n\nlet User = object({\n  name: string(),\n  bio: optional(string()), // accepts undefined\n  role: defaulted(string(), 'user'), // fills in 'user' when undefined\n  age: number(),\n})\n```\n\nUnknown keys are stripped by default. Change this with `unknownKeys`:\n\n```ts\nobject({ name: string() }, { unknownKeys: 'passthrough' }) // keeps unknown keys\nobject({ name: string() }, { unknownKeys: 'error' }) // rejects unknown keys\n```\n\n## Collections\n\n```ts\nimport { array, tuple, record, map, set, string, number, boolean } from 'remix/data-schema'\n\narray(number()) // number[]\ntuple([string(), number(), boolean()]) // [string, number, boolean]\nrecord(string(), number()) // Record<string, number>\nmap(string(), number()) // Map<string, number>\nset(number()) // Set<number>\n```\n\n## Modifiers\n\n```ts\nimport { nullable, optional, defaulted, string, number } from 'remix/data-schema'\n\nnullable(string()) // string | null\noptional(number()) // number | undefined\ndefaulted(string(), 'n/a') // fills 'n/a' when undefined\n```\n\n## Instance checks\n\n```ts\nimport { instanceof_, object } from 'remix/data-schema'\n\nlet Schema = object({\n  created: instanceof_(Date),\n  pattern: instanceof_(RegExp),\n})\n```\n\n## Any\n\nAccept any value without validation. Useful when part of a structure is opaque.\n\n```ts\nimport { any, object, string } from 'remix/data-schema'\n\nlet Envelope = object({\n  type: string(),\n  payload: any(),\n})\n```\n\n## Custom rules with `.refine()`\n\nAdd domain-specific validation logic inline. The predicate runs after the schema validates.\n\n```ts\nimport { number, string, object } from 'remix/data-schema'\n\nlet Profile = object({\n  username: string().refine((s) => s.length >= 3, 'Too short'),\n  age: number().refine((n) => n >= 18, 'Must be an adult'),\n})\n```\n\n## Output transforms with `.transform()`\n\nMap a validated value into the shape your app wants. The transformer runs after the schema validates and changes the parsed output type.\n\n```ts\nimport { object, parse, string } from 'remix/data-schema'\n\nlet Event = object({\n  id: string(),\n  createdAt: string()\n    .refine((value) => !Number.isNaN(Date.parse(value)), 'Expected valid date')\n    .transform((value) => new Date(value)),\n})\n\nlet event = parse(Event, {\n  id: 'evt_1',\n  createdAt: '2026-04-25T00:00:00.000Z',\n})\n\nevent.createdAt // Date\n```\n\nUse `.refine()` for checks that reject values without changing them. Use `.transform()` for safe, synchronous mappings; thrown errors are propagated instead of converted into validation issues.\n\n## Validation pipelines with `.pipe()`\n\nCompose reusable `Check` objects for common constraints.\n\n```ts\nimport { object, string, number } from 'remix/data-schema'\nimport { minLength, maxLength, email, min, max } from 'remix/data-schema/checks'\n\nlet Credentials = object({\n  username: string().pipe(minLength(3), maxLength(20)),\n  email: string().pipe(email()),\n  age: number().pipe(min(13), max(130)),\n})\n```\n\nBuilt-in checks: `minLength`, `maxLength`, `email`, `url`, `min`, `max`.\n\n## Coercing input values\n\nTurn stringly-typed inputs (like form data or query strings) into real types at the schema boundary.\n\n```ts\nimport { object, parse } from 'remix/data-schema'\nimport * as coerce from 'remix/data-schema/coerce'\n\nlet Query = object({\n  page: coerce.number(),\n  includeArchived: coerce.boolean(),\n  since: coerce.date(),\n  limit: coerce.bigint(),\n  search: coerce.string(),\n})\n\nlet query = parse(Query, {\n  page: '2',\n  includeArchived: 'true',\n  since: '2025-01-01',\n  limit: '100',\n  search: 42,\n})\n```\n\n## Discriminated unions\n\nPick the right schema based on a discriminator property.\n\n```ts\nimport { literal, number, object, string, variant } from 'remix/data-schema'\n\nlet Event = variant('type', {\n  created: object({ type: literal('created'), id: string() }),\n  updated: object({ type: literal('updated'), id: string(), version: number() }),\n})\n```\n\n## Recursive schemas\n\nModel trees and self-referencing structures. `lazy()` defers schema resolution to avoid circular references.\n\n```ts\nimport { array, object, string } from 'remix/data-schema'\nimport { lazy } from 'remix/data-schema/lazy'\nimport type { Schema } from 'remix/data-schema'\n\ntype TreeNode = { id: string; children: TreeNode[] }\n\nlet Node: Schema<unknown, TreeNode> = lazy(() => object({ id: string(), children: array(Node) }))\n```\n\n## Aborting early\n\nBy default, validation collects all issues in a single pass. To stop at the first issue, enable `abortEarly`.\n\n```ts\nimport { object, string, number, parseSafe } from 'remix/data-schema'\n\nlet result = parseSafe(\n  object({ name: string(), age: number() }),\n  { name: 123, age: 'x' },\n  { abortEarly: true },\n)\n\nif (!result.success) {\n  console.log(result.issues) // only the first issue\n}\n```\n\n## Type inference\n\nExtract input and output types from any Standard Schema-compatible schema.\n\n```ts\nimport { object, string, number } from 'remix/data-schema'\nimport type { InferInput, InferOutput } from 'remix/data-schema'\n\nlet User = object({ name: string(), age: number() })\n\ntype UserInput = InferInput<typeof User> // unknown\ntype UserOutput = InferOutput<typeof User> // { name: string; age: number }\n```\n\n## Extending data-schema\n\nBuild custom schemas using `createSchema`, `createIssue`, and `fail`. These are the same primitives used internally by every built-in schema.\n\n```ts\nimport { createSchema, createIssue, fail } from 'remix/data-schema'\nimport type { Schema } from 'remix/data-schema'\n\n// A schema that validates a non-empty trimmed string\nfunction trimmedString(): Schema<unknown, string> {\n  return createSchema(function validate(value, context) {\n    if (typeof value !== 'string') {\n      return fail('Expected string', context.path)\n    }\n\n    let trimmed = value.trim()\n\n    if (trimmed.length === 0) {\n      return fail('Expected non-empty string', context.path)\n    }\n\n    return { value: trimmed }\n  })\n}\n\n// A schema that validates a [lat, lng] coordinate pair\nfunction latLng(): Schema<unknown, [number, number]> {\n  return createSchema(function validate(value, context) {\n    if (!Array.isArray(value) || value.length !== 2) {\n      return fail('Expected [lat, lng] pair', context.path)\n    }\n\n    let issues = []\n    let [lat, lng] = value\n\n    if (typeof lat !== 'number' || lat < -90 || lat > 90) {\n      issues.push(createIssue('Latitude must be between -90 and 90', [...context.path, 0]))\n    }\n\n    if (typeof lng !== 'number' || lng < -180 || lng > 180) {\n      issues.push(createIssue('Longitude must be between -180 and 180', [...context.path, 1]))\n    }\n\n    if (issues.length > 0) {\n      return { issues }\n    }\n\n    return { value: [lat, lng] }\n  })\n}\n```\n\nThe validator function receives the raw value and a context with the current `path` and `options`. Return `{ value }` on success or `{ issues: [...] }` on failure. The returned schema is fully Standard Schema v1-compatible and supports `.pipe()` and `.refine()` out of the box.\n\n## License\n\nSee [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)\n","readmeFilename":"README.md"}