# schema - one declaration, two outputs Source: https://truecopy.dev/docs/schema/ Module: truecopy/schema Write the schema once and get the check and the record type. Or bring the one you already wrote in Zod, Valibot or ArkType. A reader written naturally is **opportunistic**: it looks for signals (a year, a number, an amount) and takes what it finds. The only refusal it can put into words is "I found nothing", so it accepts anything that happens to carry the shape of what it seeks. Measured before this existed: a meeting report mentioning 2019 and four sites came back as a career record, six findings, one of them at the highest severity. The schema reverses the burden of proof. ```ts import { schemaOf, validate, type RecordOf } from 'truecopy/schema'; export const CAREER = schemaOf({ name: 'career-record', fields: { year: { format: 'year', minimum: 1930, maximum: 2030, required: true }, quarters: { format: 'integer', minimum: 0, maximum: 4, required: false }, pay: { format: 'number', minimum: 0, required: false } }, minimumRecords: 5, key: 'year' }); export type CareerRow = RecordOf; // { year: number; quarters: number | null; pay: number | null } ``` **One declaration, two outputs.** A shape declared twice drifts, and the half that drifts silently is always the check. ```ts const violation = validate(CAREER, rows); // null // { cause: 'too-few-records', conforming: 1, expected: 5 } // { cause: 'out-of-format', field: 'year', value: 2199, record: … } ``` What is missing is **named**, never guessed: this library says which rule broke, your application writes the sentence in its own voice. ## Format before count An absurd value says the document was cut up wrongly, and counting the records of a wrong cut tells you nothing. So `validate` reports the format failure first, and reports **one** rule, because a screen listing three failures is a screen nobody reads. ## The key, and what it separates `key` counts **distinct values** rather than records, and it is what tells a table from a document about one single thing. A payslip carries the vocabulary of retirement and repeats one year; a career record lines up forty. Counting records does not tell them apart. Counting years does. ## A schema is pure data No accessor, no closure, nothing that does not survive JSON. It can be served by a backend, versioned, refined by whoever reads the documents, and swapped for another market without a deploy. The cost is one rule: **a record carries each value under the field's own name.** A reader whose rows are shaped otherwise flattens them in its own code, where the mapping stays visible. ## Bring your own schema Already declared that row type in Zod, Valibot, ArkType or TypeBox? Do not declare it twice. ```ts import { validateWith } from 'truecopy/schema'; validateWith(myZodSchema, rows, { minimumRecords: 5, key: 'year' }); ``` Any [Standard Schema](https://standardschema.dev) works. What truecopy adds is the part a per-record validator cannot know: **how many well-formed records make a document.** Synchronous only, and it says so rather than quietly awaiting: a reading runs inside a render path, and turning it async would spread through every caller for the sake of validators nobody writes that way. ## This is not the kind question Whether the document is the one you expect at all belongs to [classify](https://truecopy.dev/docs/classify/), which asks it with precedence rules. Two answers to one question always end up disagreeing. Map of this site for a model: https://truecopy.dev/llms.txt Every page in one file: https://truecopy.dev/llms-full.txt