npm package: @sleekcms/json-zen
Description: Easify verify and shape JSON objects using a simplified alternative JSON schema
Author: Yusuf Bhabhrawala
Latest version: 1.6.0

# JSON Zen

A compact, string-based schema syntax for validating and shaping JSON objects — with far less boilerplate than JSON Schema.

Try it live at **[json-zen.sleekcms.site](https://json-zen.sleekcms.site/)**.

## Installation

```bash
npm install @sleekcms/json-zen
```

```js
// CommonJS
const { verify, check, shape, toSchema, extendTypes } = require('@sleekcms/json-zen');

// ESM / TypeScript
import { verify, check, shape, toSchema, extendTypes } from '@sleekcms/json-zen';
```

## Quick Start

```js
import { verify, check, shape, toSchema } from '@sleekcms/json-zen';

const data = { name: 'Alice', age: 30, tags: ['admin', 'user'] };

// Validate (throws on failure)
verify(data, '{name:s, age:n, tags:[s]}'); // true
// using full type names is equivalent:
verify(data, '{name:string, age:number, tags:[string]}'); // true

// Safe boolean check
check(data, '{name:s, age:s}'); // false

// Generate a schema from an existing object
toSchema(data); // "{name:string,age:number,tags:[string]}"

// Fill in missing fields with typed defaults
shape({ name: 'Bob' }, '{name:s, age:n, active:b}');
// { name: 'Bob', age: 0, active: true }
```

---

## Schema Syntax

Alt schemas are plain strings. Whitespace is ignored.

### Built-in Types

| Shorthand | Full name | Validates            | Default (shape) |
|-----------|-----------|----------------------|-----------------|
| `s`       | `string`  | string               | `""`            |
| `n`       | `number`  | number (incl. floats)| `0`             |
| `b`       | `boolean` | boolean              | `true`          |
| —         | `null`    | strictly `null`      | `null`          |
| `?`       | —         | any value / nullable | `null`          |

Shorthands and full names are interchangeable: `{a:s}` and `{a:string}` behave identically.

### Objects

```
{key:type, key:type, ...}
```

```js
verify({ a: 'hello', b: 42 }, '{a:s, b:n}');           // true — shorthands
verify({ a: 'hello', b: 42 }, '{a:string, b:number}'); // true — full names
```

### Arrays

A single type applies to every element. Multiple comma-separated types cycle by index.

```js
verify([1, 2, 3], '[n]');           // true — all numbers
verify([1, 'x', 2, 'y'], '[n,s]');  // true — alternating number/string
```

### Optional fields (`?`)

For object properties, suffix the key with `?` to make it optional (TypeScript-style). For array elements, prefix the type with `?`. A missing or `null` value passes validation.

```js
verify({ a: 1 }, '{a:n, b?:s}');    // true — b is missing but optional
verify({ a: 1, b: null }, '{a:n, b?:s}'); // true
verify([1, null, 3], '[?n]');       // true — array elements are optional
```

### Presence-only validation

Omit the type to require only that a key exists and is not `null`/`undefined`.

```js
verify({ a: 1, b: false }, '{a, b}'); // true
verify({ a: 1, b: null }, '{a, b}');  // throws — b is null
```

### Default values (`type:default`)

Attach a default after the type, used by `shape` to fill missing values.

```js
shape({}, '{name:s:anonymous, count:n:0}');
// { name: 'anonymous', count: 0 }
```

Use quotes for defaults containing spaces:

```js
shape({}, '{title:s:"Hello World"}');
// { title: 'Hello World' }
```

### Wildcard key (`*`)

`*:type` matches any key not already listed in the schema.

```js
verify({ a: 1, b: 2, c: 3 }, '{*:n}');         // true
verify({ a: 1, b: '2' }, '{a:n, *:s}');         // true — b matched by wildcard
shape({ x: 1, y: 2, z: 3 }, '{*:n}');           // { x: 1, y: 2, z: 3 }
```

### Nested objects and arrays

```js
const schema = '{user:{name:s, scores:[n]}, active:b}';

verify({ user: { name: 'Alice', scores: [10, 20] }, active: true }, schema); // true
```

---

## API

### `verify(json, schema, options?)`

Validates `json` against `schema`. Returns `true` on success, throws an `ZenError` on failure.

```js
verify({ a: 1 }, '{a:s}');
// throws: "json.a: validation failed"
```

Access all failures at once via `error.errors`:

```js
try {
  verify({}, '{a:n, b:n}');
} catch (e) {
  console.log(e.message); // "Validation failed: 2 errors found"
  console.log(e.errors);  // [['json.a', 'is required'], ['json.b', 'is required']]
}
```

**Options:**

| Option  | Type     | Description                                       |
|---------|----------|---------------------------------------------------|
| `_path` | `string` | Override the root label in error messages         |
| _(key)_ | validator | Inline custom type — see [Custom Validators](#custom-validators) |

```js
verify({ a: 'x' }, '{a:s}', { _path: 'payload' });
// throws: "payload.a: validation failed"
```

---

### `check(json, schema, options?)`

Same as `verify` but returns `false` instead of throwing. Schema errors still throw.

```js
check({ a: 1, b: 'x' }, '{a:n, b:n}'); // false
check({ a: 1, b: 2 }, '{a:n, b:n}');   // true
```

---

### `shape(json, schema, options?)`

Returns a new object shaped to the schema. Values from `json` are used when valid; otherwise, typed defaults fill in the gaps. Extra keys in `json` not in the schema are omitted.

```js
shape({ a: 1, extra: 99 }, '{a:n, b:s, c:b}');
// { a: 1, b: '', c: true }
```

Nested structures are shaped recursively:

```js
shape({ a: {} }, '{a:{x:n, y:s}}');
// { a: { x: 0, y: '' } }
```

**Options:**

| Option      | Type      | Description                                                  |
|-------------|-----------|--------------------------------------------------------------|
| `fillNulls` | `boolean` | When `true`, fill optional (`?`) fields with defaults instead of `null` |
| _(key)_     | validator | Inline custom type                                           |

```js
shape({}, '{a?:n}');                   // { a: null }
shape({}, '{a?:n}', { fillNulls: true }); // { a: 0 }
```

---

### `toSchema(json, options?)`

Generates a schema string from a JSON object. Useful for bootstrapping a schema from a known-good example.

```js
toSchema({ a: 'foo', b: 1, c: [1, 2], d: { e: null } });
// "{a:string,b:number,c:[number],d:{e:?}}"
```

**Options:**

| Option    | Type      | Default | Description |
|-----------|-----------|---------|-------------|
| `default` | `string`  | —       | A type name to treat as implicit. Object attributes whose inferred type matches this value will omit the `:type` suffix in the output. |
| `compact` | `boolean` | `false` | When `true`, omits the `:type` suffix for every scalar attribute in objects (equivalent to setting `default` to all types at once). |

```js
toSchema({ name: 'Alice', age: 30, active: true });
// "{name:string,age:number,active:boolean}"

// Omit ":string" everywhere — handy when most fields are strings
toSchema({ name: 'Alice', age: 30, active: true }, { default: 'string' });
// "{name,age:number,active:boolean}"

// Strip all :type annotations
toSchema({ name: 'Alice', age: 30, active: true }, { compact: true });
// "{name,age,active}"
```

---

### `extendTypes(validators)`

Registers one or more custom types globally. Once registered, the type name can be used in any schema.

The validator function receives the value when checking, and `undefined` when `shape` needs a default — return the default value in that case.

```js
import { extendTypes, shape, verify } from '@sleekcms/json-zen';

extendTypes({
  url: (value) => {
    if (value === undefined) return 'https://example.com'; // default for shape
    return typeof value === 'string' && value.startsWith('http');
  }
});

verify({ img: 'https://example.com/pic.jpg' }, '{img:url}'); // true
shape({}, '{img:url}'); // { img: 'https://example.com' }
```

---

## Custom Validators

Custom validators can be passed inline via `options`, registered globally via `extendTypes`, or provided as enum arrays or RegExp patterns.

### Inline function

```js
verify({ status: 'active' }, '{status:myType}', {
  myType: (value) => value === 'active' || value === 'inactive'
});
```

### Enum (array)

```js
verify({ role: 'admin' }, '{role:r}', { r: ['admin', 'user', 'guest'] }); // true
verify({ role: 'root' },  '{role:r}', { r: ['admin', 'user', 'guest'] }); // throws
```

### Regex

```js
verify({ slug: 'hello-world' }, '{slug:sl}', { sl: /^[a-z-]+$/ }); // true
```

### Context-aware validator

Validators receive `{ path, json, parent }` as a second argument, enabling cross-field validation:

```js
extendTypes({
  matchesType: (value, { parent }) => {
    if (parent.type === 'number') return typeof value === 'number';
    if (parent.type === 'string') return typeof value === 'string';
    return false;
  }
});

verify(
  [{ type: 'number', value: 42 }, { type: 'string', value: 'hi' }],
  '[{type:s, value:matchesType}]'
); // true
```

---

## License

MIT © Yusuf Bhabhrawala, SleekSky LLC