# core/validators/schema.validator.ts

> 67 lines of code and 11 definitions.

Tree: GovLab Patterns
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/patterns#file-patterns-core-validators-schema-validator-ts
Source text: https://banes-lab.com/source/patterns/core/validators/schema.validator.ts.txt

Listed in [core/validators](https://banes-lab.com/api/source/patterns/core/validators.md), after [core/validators/report.validator.ts](https://banes-lab.com/source/patterns/core/validators/report.validator.ts.md).

## Definitions

- `sortedList` (lexical_declaration, line 25)
- `validateField` (lexical_declaration, line 29)
- `scalarMatches` (lexical_declaration, line 21)
- `validateRecord` (lexical_declaration, line 46, exported)
- `validateMapping` (lexical_declaration, line 64, exported)
- `ACCEPTED` (lexical_declaration, line 14)
- `value` (lexical_declaration, line 30)
- `kind` (lexical_declaration, line 37)
- `allowed` (lexical_declaration, line 53, exported)
- `fields` (lexical_declaration, line 68, exported)
- `unknown` (lexical_declaration, line 69, exported)

## Contained in

- [core/validators](https://banes-lab.com/anatomy/patterns/folder-patterns-core-validators.md)

## Uses

- [configuration/strings/schema.strings.ts](https://banes-lab.com/source/patterns/configuration/strings/schema.strings.ts.md)
- [core/classifiers/schema.classifier.ts](https://banes-lab.com/source/patterns/core/classifiers/schema.classifier.ts.md)
- [core/predicates/record.predicate.ts](https://banes-lab.com/source/patterns/core/predicates/record.predicate.ts.md)

## Linked from

- [configuration/strings](https://banes-lab.com/anatomy/patterns/folder-patterns-configuration-strings.md)
- [core/classifiers](https://banes-lab.com/anatomy/patterns/folder-patterns-core-classifiers.md)
- [core/predicates](https://banes-lab.com/anatomy/patterns/folder-patterns-core-predicates.md)
- [core/validators](https://banes-lab.com/anatomy/patterns/folder-patterns-core-validators.md)

## Source

```typescript
import { DataLoadError, isBlank, kindOf } from "#core/classifiers/schema.classifier";
import type { FieldSchema, Primitive } from "#types/schema.types";
import {
    NULL_PRIMITIVE,
    kindMismatch,
    notAnObject,
    primitiveMismatch,
    requiredField,
    unknownMappingFields,
    unknownRecordFields,
} from "#configuration/strings/schema.strings";
import { isRecord } from "#core/predicates/record.predicate";

const ACCEPTED: Readonly<Record<Primitive, ReadonlySet<string>>> = {
    boolean: new Set(["boolean"]),
    float: new Set(["number"]),
    integer: new Set(["number"]),
    string: new Set(["string"]),
};

const scalarMatches = function scalarMatches(value: unknown, primitive: Primitive | null): boolean {
    return primitive === null || ACCEPTED[primitive].has(typeof value);
};

const sortedList = function sortedList(names: readonly string[]): string {
    return names.toSorted((a, b) => a.localeCompare(b)).join(", ");
};

const validateField = function validateField(field: FieldSchema, record: Record<string, unknown>): void {
    const value = record[field.name];
    if (isBlank(value)) {
        if (!field.nullable) {
            throw new DataLoadError(requiredField(field.name));
        }
        return;
    }
    const kind = kindOf(value);
    if (kind !== field.kind) {
        throw new DataLoadError(kindMismatch(field.name, field.kind, kind));
    }
    if (kind === "scalar" && !scalarMatches(value, field.primitive)) {
        throw new DataLoadError(primitiveMismatch(field.name, field.primitive ?? NULL_PRIMITIVE));
    }
};

export const validateRecord = function validateRecord(
    record: unknown,
    schema: readonly FieldSchema[],
): Record<string, unknown> {
    if (!isRecord(record)) {
        throw new DataLoadError(notAnObject(JSON.stringify(record)));
    }
    const allowed = new Set(schema.map((field) => field.name));
    const unknown = Object.keys(record).filter((key) => !allowed.has(key));
    if (unknown.length > 0) {
        throw new DataLoadError(unknownRecordFields(sortedList(unknown)));
    }
    for (const field of schema) {
        validateField(field, record);
    }
    return record;
};

export const validateMapping = function validateMapping(
    mapping: ReadonlyMap<string, readonly string[]>,
    schema: readonly FieldSchema[],
): void {
    const fields = new Set(schema.map((field) => field.name));
    const unknown = [...mapping.keys()].filter((name) => !fields.has(name));
    if (unknown.length > 0) {
        throw new DataLoadError(unknownMappingFields(sortedList(unknown)));
    }
};
```
