@toomuchdesign/json-schema-fns
v1.0.0
Published
Immutable, type-safe utilities for transforming and composing JSON Schemas.
Maintainers
Readme
@toomuchdesign/json-schema-fns
@toomuchdesign/json-schema-fns is a type-safe immutable utility library for transforming JSON Schemas.
It ensures that schema transformations not only update the runtime schema correctly but also preserve accurate TypeScript type inference. This makes it especially useful when paired with tools like json-schema-to-ts.
import { omitProps } from '@toomuchdesign/json-schema-fns';
import type { FromSchema } from 'json-schema-to-ts';
const userSchema = {
type: 'object',
properties: {
id: { type: 'string' },
password: { type: 'string' },
},
required: ['id', 'password'],
} as const;
const publicUserSchema = omitProps(userSchema, ['password']);
type PublicUser = FromSchema<typeof publicUserSchema>;
// { id: string }Why?
Manipulating JSON Schemas directly can be verbose and error-prone.
This library provides small, focused utilities that keep runtime schemas and TypeScript types in sync — especially when paired with json-schema-to-ts.
Installation
npm install @toomuchdesign/json-schema-fnsAPI
Try it live ⚡ — every example below runs in the playground; open the file matching the function name.
All transformations preserve every keyword they don't explicitly touch. Input metadata like title, description, default, examples, $id, $defs, and any other keyword the library does not transform rides through to the output unchanged. The Touches column below lists the only keywords each function modifies.
| Function | Description | Touches |
| --------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------- |
| omitProps | Omit specific properties from an object JSON schema | properties, required |
| omitPropsDeep | Omit specific nested properties using dot-notation paths | properties, required |
| pickProps | Pick only specific properties from an object JSON schema | properties, required |
| pickPropsDeep | Pick specific nested properties using dot-notation paths | properties, required |
| mergeProps | Merge two object JSON schemas' properties and patternProperties into one | properties, patternProperties, required |
| renameProps | Rename specific properties via an { oldKey: newKey } map | properties, required |
| renamePropsDeep | Rename specific nested properties using dot-notation paths | properties, required |
| requireProps | Mark specific properties as required (all if none provided) | required |
| optionalProps | Make specific properties optional (all if none provided) | required |
| sealSchemaDeep | Recursively set additionalProperties: false on all object schemas | additionalProperties |
| unsealSchemaDeep | Recursively remove additionalProperties and unevaluatedProperties | additionalProperties, unevaluatedProperties |
omitProps
Omit specific properties from an object JSON schema.
import { omitProps } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
required: ['a', 'b'],
} as const;
const result = omitProps(schema, ['b']);omitPropsDeep
Omit specific nested properties from an object JSON schema using dot-notation paths. Paths sharing a common prefix are merged; a bare key drops the whole sub-schema, a dotted path drills in and drops only the leaf. When both forms target the same key, the bare-key drop wins.
import { omitPropsDeep } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
required: ['user', 'meta'],
properties: {
user: {
type: 'object',
required: ['id', 'password'],
properties: {
id: { type: 'string' },
password: { type: 'string' },
},
},
meta: { type: 'string' },
},
} as const;
const result = omitPropsDeep(schema, ['user.password', 'meta']);pickProps
Pick only specific properties from an object JSON schema.
import { pickProps } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'number' },
},
required: ['a', 'b'],
} as const;
const result = pickProps(schema, ['b']);pickPropsDeep
Pick specific nested properties from an object JSON schema using dot-notation paths. Paths sharing a common prefix are merged; a bare key (without a sub-path) keeps the whole sub-schema unchanged.
import { pickPropsDeep } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
required: ['user', 'meta'],
properties: {
user: {
type: 'object',
required: ['id', 'password'],
properties: {
id: { type: 'string' },
password: { type: 'string' },
},
},
meta: { type: 'string' },
},
} as const;
const result = pickPropsDeep(schema, ['user.id', 'meta']);mergeProps
Merge two object JSON schemas properties and patternProperties props into one. If the same property key exists in both schemas, the property from schema2 takes precedence. Top-level metadata (title, description, $id, etc.) follows the same rule — values from schema2 overwrite schema1 via spread. Combinators (allOf / anyOf / oneOf / not) are not merged; see docs/combinators.md.
import { mergeProps } from '@toomuchdesign/json-schema-fns';
const schema1 = {
type: 'object',
properties: {
a: { type: 'string' },
},
required: ['a'],
} as const;
const schema2 = {
type: 'object',
properties: {
b: { type: 'number' },
},
required: ['b'],
} as const;
const result = mergeProps(schema1, schema2);renameProps
Rename specific properties in an object JSON schema via an { oldKey: newKey } map. Source keys must exist in schema.properties (compile error on unknown keys); target keys are arbitrary strings. Position in required is preserved. Shallow only.
import { renameProps } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
properties: {
id: { type: 'string' },
email: { type: 'string' },
},
required: ['id', 'email'],
} as const;
const result = renameProps(schema, { id: 'userId', email: 'emailAddress' });renamePropsDeep
Rename specific nested properties in an object JSON schema using dot-notation paths. Source paths must resolve to existing properties (compile error on unknown paths); target names are arbitrary strings — renames don't move properties between levels. Bare and dotted entries can coexist: { user: 'account', 'user.id': 'userId' } renames user at the top level and id within it. Position in each level's required is preserved.
import { renamePropsDeep } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
required: ['user', 'meta'],
properties: {
user: {
type: 'object',
required: ['id', 'email'],
properties: {
id: { type: 'string' },
email: { type: 'string' },
},
},
meta: { type: 'string' },
},
} as const;
const result = renamePropsDeep(schema, {
'user.id': 'userId',
meta: 'metadata',
});requireProps
Mark specific properties in a object JSON schema as required. If no keys provided, all properties become required.
import { requireProps } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'string' },
c: { type: 'string' },
},
required: ['b'],
} as const;
const result = requireProps(schema, ['a', 'c']);optionalProps
Make specific properties of a object JSON schema optional. If no keys provided, all properties become optional.
import { optionalProps } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'string' },
c: { type: 'string' },
},
required: ['a', 'b', 'c'],
} as const;
const result = optionalProps(schema, ['b', 'c']);sealSchemaDeep
Recursively set additionalProperties: false on all object JSON schema schemas.
It does not modify JSON Schema combinators such as allOf, anyOf, oneOf, or not. This ensures that the logical combination of schemas remains intact and that the semantics of the schema are not altered in any way.
import { sealSchemaDeep } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
required: ['name'],
properties: {
name: { type: 'string' },
address: {
type: 'object',
properties: {
street: { type: 'string' },
},
},
},
} as const;
const result = sealSchemaDeep(schema);unsealSchemaDeep
Recursively remove additionalProperties and unevaluatedProperties keywords from all object JSON schema schemas.
It does not modify JSON Schema combinators such as allOf, anyOf, oneOf, or not. This ensures that the logical combination of schemas remains intact and that the semantics of the schema are not altered in any way.
import { unsealSchemaDeep } from '@toomuchdesign/json-schema-fns';
const schema = {
type: 'object',
additionalProperties: false,
required: ['name'],
properties: {
name: { type: 'string' },
address: {
type: 'object',
additionalProperties: false,
properties: {
street: { type: 'string' },
},
},
},
} as const;
const result = unsealSchemaDeep(schema);Composition & pipe-friendly API
In addition to the standard functional API, json-schema-fns also offers a composition-friendly counterpart that enables schema transformations through a pipeable interface. Live demo ⚡
Pipelines of at least 9 transformations are verified across all four endorsed pipe libraries (limited by pipe-ts's typed-overload cap of 9); remeda, effect, and ts-functional-pipe support deeper pipelines. See test/composition-pipe-depth-ceiling.test.ts for per-library ceilings.
Note: piping could lead TypeScript to hit its internal recursion limits producing the following error:
TS2589: Type instantiation is excessively deep and possibly infinite
Note: the library does not include its own pipe utility. You are free to use any composition library you prefer. See composition tests for integration with each library.
import {
pipeMergeProps,
pipeOmitProps,
pipeRequireProps,
pipeSealSchemaDeep,
} from '@toomuchdesign/json-schema-fns';
import { pipeWith } from 'pipe-ts';
const schema = {
type: 'object',
properties: {
a: { type: 'string' },
b: { type: 'string' },
},
required: ['a'],
} as const;
const result = pipeWith(
schema,
pipeMergeProps({
type: 'object',
properties: {
c: { type: 'string' },
},
}),
pipeOmitProps(['a']),
pipeRequireProps(['b']),
pipeSealSchemaDeep(),
);Supported JSON Schema dialect
The library targets JSON Schema Draft-07 (2018). Every transformation acts on a small known set of Draft-07 keywords (type, properties, patternProperties, required, additionalProperties, the combinators allOf / anyOf / oneOf / not, and the conditional applicators if / then / else).
Forward-compatibility is a design property. Schemas that use newer keywords from Draft 2019-09 or Draft 2020-12 — for example $defs, prefixItems, dependentRequired, propertyNames, $dynamicRef — pass through the library unchanged: every keyword the library does not explicitly transform rides through via TypeScript's const generic + Omit-based merge. See docs/types.md for the mechanics.
Type compatibility with json-schema-to-ts
The library accepts a homegrown JSONSchemaObject input type (kept separate from json-schema-to-ts's JSONSchema for type-performance reasons). In practice the two are compatible: any as const schema literal that is valid under json-schema-to-ts's JSONSchema type and uses only the keywords listed above is accepted as input, and every output schema is consumable by FromSchema. This is what makes FromSchema<typeof transformed> work in the example at the top of this README without any adapter.
Related projects
- https://github.com/codeperate/json-schema-builder
- https://github.com/livelybone/union-tuple
- https://github.com/ksxnodemodules/typescript-tuple
Contributing
Contributions are welcome — see CONTRIBUTING.md for the workflow, local checks, and versioning policy. Before opening a PR, please run:
npx changeset