@cleverbrush/schema-json 0.0.0-beta-20260410073748
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -0
- package/dist/fromJsonSchema.d.ts +86 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/standardJsonSchema.d.ts +31 -0
- package/dist/toJsonSchema.d.ts +63 -0
- package/dist/types.d.ts +257 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# @cleverbrush/schema-json
|
|
2
|
+
<!-- coverage-badge-start -->
|
|
3
|
+

|
|
4
|
+
<!-- coverage-badge-end -->
|
|
5
|
+
|
|
6
|
+
Bidirectional JSON Schema (Draft 7 / 2020-12) interop for
|
|
7
|
+
[`@cleverbrush/schema`](../schema).
|
|
8
|
+
|
|
9
|
+
Import a JSON Schema and get a fully-typed `@cleverbrush/schema` builder that
|
|
10
|
+
preserves every constraint. Or convert a builder back to a JSON Schema object
|
|
11
|
+
for use in OpenAPI specs, form generators, or any other JSON Schema consumer.
|
|
12
|
+
|
|
13
|
+
## When to use this library
|
|
14
|
+
|
|
15
|
+
- **Consuming external APIs** — you have a JSON Schema from an OpenAPI spec or
|
|
16
|
+
a third-party service and want to validate incoming data with full TypeScript
|
|
17
|
+
type inference.
|
|
18
|
+
- **OpenAPI / JSON Schema round-trip** — generate JSON Schemas from your
|
|
19
|
+
`@cleverbrush/schema` validators to embed in API specs.
|
|
20
|
+
- **Migrating from raw JSON Schema** — convert an existing schema catalogue to
|
|
21
|
+
`@cleverbrush/schema` builders incrementally.
|
|
22
|
+
- **Code generation** — introspect a JSON Schema at the type level via
|
|
23
|
+
`JsonSchemaNodeToBuilder<S>` without running any code.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @cleverbrush/schema-json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
> **Peer dependency:** `@cleverbrush/schema@^2.0.0` must already be installed.
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { fromJsonSchema, toJsonSchema } from '@cleverbrush/schema-json';
|
|
37
|
+
import { object, string, number } from '@cleverbrush/schema';
|
|
38
|
+
|
|
39
|
+
// ── JSON Schema → builder ──────────────────────────────────────────────────
|
|
40
|
+
const PersonSchema = fromJsonSchema({
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
name: { type: 'string', minLength: 1 },
|
|
44
|
+
email: { type: 'string', format: 'email' },
|
|
45
|
+
age: { type: 'integer', minimum: 0 },
|
|
46
|
+
},
|
|
47
|
+
required: ['name', 'email'],
|
|
48
|
+
} as const); // ← `as const` is required for precise type inference
|
|
49
|
+
|
|
50
|
+
const result = PersonSchema.parse({ name: 'Alice', email: 'alice@example.com', age: 30 });
|
|
51
|
+
// result.object is typed as { name: string; email: string; age?: number }
|
|
52
|
+
|
|
53
|
+
// ── Builder → JSON Schema ──────────────────────────────────────────────────
|
|
54
|
+
const ApiSchema = object({
|
|
55
|
+
id: string().uuid(),
|
|
56
|
+
title: string().minLength(1).maxLength(255),
|
|
57
|
+
score: number().min(0).max(100).optional(),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const spec = toJsonSchema(ApiSchema);
|
|
61
|
+
// {
|
|
62
|
+
// "$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
63
|
+
// "type": "object",
|
|
64
|
+
// "properties": {
|
|
65
|
+
// "id": { "type": "string", "format": "uuid" },
|
|
66
|
+
// "title": { "type": "string", "minLength": 1, "maxLength": 255 },
|
|
67
|
+
// "score": { "type": "number" }
|
|
68
|
+
// },
|
|
69
|
+
// "required": ["id", "title"]
|
|
70
|
+
// }
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API Reference
|
|
74
|
+
|
|
75
|
+
### `fromJsonSchema(schema)`
|
|
76
|
+
|
|
77
|
+
Converts a JSON Schema literal to a `@cleverbrush/schema` builder.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
function fromJsonSchema<const S>(schema: S): JsonSchemaNodeToBuilder<S>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Parameter | Type | Description |
|
|
84
|
+
| --- | --- | --- |
|
|
85
|
+
| `schema` | JSON Schema literal | Pass with `as const` for precise TypeScript inference |
|
|
86
|
+
|
|
87
|
+
**Returns** a `@cleverbrush/schema` builder (e.g. `StringSchemaBuilder`,
|
|
88
|
+
`ObjectSchemaBuilder`, `UnionSchemaBuilder`, etc.) whose static type mirrors the
|
|
89
|
+
JSON Schema structure.
|
|
90
|
+
|
|
91
|
+
#### `as const` is required
|
|
92
|
+
|
|
93
|
+
Without `as const`, TypeScript widens string literals to `string` and object
|
|
94
|
+
shapes to `Record<string, unknown>`, so inference collapses to
|
|
95
|
+
`SchemaBuilder<unknown>`. Always annotate:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const S = { type: 'object', properties: { x: { type: 'number' } } } as const;
|
|
99
|
+
const schema = fromJsonSchema(S); // ObjectSchemaBuilder<{ x: NumberSchemaBuilder<...> }>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Supported JSON Schema keywords
|
|
103
|
+
|
|
104
|
+
| Keyword | Builder equivalent |
|
|
105
|
+
| ------------------------------------------ | ----------------------------------- |
|
|
106
|
+
| `type: 'string'` | `string()` |
|
|
107
|
+
| `type: 'number'` | `number()` |
|
|
108
|
+
| `type: 'integer'` | `number()` with integer flag |
|
|
109
|
+
| `type: 'boolean'` | `boolean()` |
|
|
110
|
+
| `type: 'null'` | `SchemaBuilder<null>` |
|
|
111
|
+
| `type: 'array'` + `items` | `array(itemBuilder)` |
|
|
112
|
+
| `type: 'object'` + `properties` | `object({ … })` |
|
|
113
|
+
| `required: […]` | required / optional per property |
|
|
114
|
+
| `additionalProperties: true` | `.acceptUnknownProps()` |
|
|
115
|
+
| `const` | literal builder (`.equals(...)`) |
|
|
116
|
+
| `enum` | `union(…)` of const builders |
|
|
117
|
+
| `anyOf` | `union(…)` of sub-builders |
|
|
118
|
+
| `allOf` | not supported — falls back to `any()` |
|
|
119
|
+
| `minLength` / `maxLength` | `.minLength()` / `.maxLength()` |
|
|
120
|
+
| `pattern` | `.matches(regex)` (invalid patterns silently ignored) |
|
|
121
|
+
| `minimum` / `maximum` | `.min()` / `.max()` |
|
|
122
|
+
| `exclusiveMinimum` / `exclusiveMaximum` | custom validator (not round-trippable via `toJsonSchema`) |
|
|
123
|
+
| `multipleOf` | `.multipleOf()` |
|
|
124
|
+
| `minItems` / `maxItems` | `.minLength()` / `.maxLength()` on array |
|
|
125
|
+
| `format: 'email'` | `.email()` extension |
|
|
126
|
+
| `format: 'uuid'` | `.uuid()` extension |
|
|
127
|
+
| `format: 'uri'` or `'url'` | `.url()` extension |
|
|
128
|
+
| `format: 'ipv4'` | `.ip({ version: 'v4' })` |
|
|
129
|
+
| `format: 'ipv6'` | `.ip({ version: 'v6' })` |
|
|
130
|
+
| `format: 'date-time'` | `.matches(iso8601 regex)` |
|
|
131
|
+
| `readOnly: true` | `.readonly()` |
|
|
132
|
+
| `description` | `.describe(text)` |
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
### `toJsonSchema(schema, opts?)`
|
|
137
|
+
|
|
138
|
+
Converts a `@cleverbrush/schema` builder to a JSON Schema object.
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
function toJsonSchema(
|
|
142
|
+
schema: SchemaBuilder<any, any, any>,
|
|
143
|
+
opts?: ToJsonSchemaOptions,
|
|
144
|
+
): Record<string, unknown>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
| Parameter | Type | Description |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| `schema` | `SchemaBuilder` | any builder from `@cleverbrush/schema` |
|
|
150
|
+
| `opts` | `ToJsonSchemaOptions` | optional output configuration |
|
|
151
|
+
|
|
152
|
+
**Returns** a plain JavaScript object that is safe to `JSON.stringify`.
|
|
153
|
+
|
|
154
|
+
Descriptions set via `.describe(text)` are emitted as the `description` field on the corresponding JSON Schema node (including nested object properties).
|
|
155
|
+
|
|
156
|
+
#### `ToJsonSchemaOptions`
|
|
157
|
+
|
|
158
|
+
| Option | Type | Default | Description |
|
|
159
|
+
| --- | --- | --- | --- |
|
|
160
|
+
| `draft` | `'2020-12' \| '07'` | `'2020-12'` | JSON Schema draft version for the `$schema` URI |
|
|
161
|
+
| `$schema` | `boolean` | `true` | Whether to include the `$schema` header in the output |
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
// Embed in OpenAPI (suppress the $schema header)
|
|
165
|
+
toJsonSchema(schema, { $schema: false });
|
|
166
|
+
|
|
167
|
+
// Use Draft 07
|
|
168
|
+
toJsonSchema(schema, { draft: '07' });
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Type Utilities
|
|
174
|
+
|
|
175
|
+
### `InferFromJsonSchema<S>`
|
|
176
|
+
|
|
177
|
+
Recursively derives the TypeScript value type from a statically-known JSON
|
|
178
|
+
Schema. Useful when you want the type without calling `fromJsonSchema` at
|
|
179
|
+
runtime.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import type { InferFromJsonSchema } from '@cleverbrush/schema-json';
|
|
183
|
+
|
|
184
|
+
const S = {
|
|
185
|
+
type: 'object',
|
|
186
|
+
properties: {
|
|
187
|
+
id: { type: 'integer' },
|
|
188
|
+
label: { type: 'string' },
|
|
189
|
+
},
|
|
190
|
+
required: ['id'],
|
|
191
|
+
} as const;
|
|
192
|
+
|
|
193
|
+
type Item = InferFromJsonSchema<typeof S>;
|
|
194
|
+
// { id: number; label?: string }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `JsonSchemaNodeToBuilder<S, TRequired?>`
|
|
198
|
+
|
|
199
|
+
Maps a statically-known JSON Schema literal to the `@cleverbrush/schema`
|
|
200
|
+
builder type — purely at the type level, no runtime code executed.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import type { JsonSchemaNodeToBuilder } from '@cleverbrush/schema-json';
|
|
204
|
+
|
|
205
|
+
const S = { type: 'string', format: 'email' } as const;
|
|
206
|
+
type B = JsonSchemaNodeToBuilder<typeof S>;
|
|
207
|
+
// StringSchemaBuilder<string, true>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Limitations
|
|
213
|
+
|
|
214
|
+
| Limitation | Notes |
|
|
215
|
+
| ----------------------------------------- | ------------------------------------------------------ |
|
|
216
|
+
| Custom validators (`addValidator`) | Not representable in JSON Schema — omitted in `toJsonSchema` output, not recoverable by `fromJsonSchema` |
|
|
217
|
+
| Preprocessors (`addPreprocessor`) | Same as above |
|
|
218
|
+
| `$ref` / `$defs` | Not supported in `fromJsonSchema` |
|
|
219
|
+
| `if` / `then` / `else` | Not supported |
|
|
220
|
+
| `not` | Not supported |
|
|
221
|
+
| `allOf` in `fromJsonSchema` | Falls back to `SchemaBuilder<unknown>` (no deep merge) |
|
|
222
|
+
| Dual IP format (`ip()` with both v4 + v6) | `format` is omitted in `toJsonSchema` output (no standard keyword covers both) |
|
|
223
|
+
| JSDoc comments on properties | Not preserved in `toJsonSchema` output |
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { JsonSchemaNodeToBuilder } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Converts a JSON Schema object into a `@cleverbrush/schema` builder.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* **`as const` is required for precise TypeScript inference.** Without it
|
|
7
|
+
* TypeScript widens string literals to `string` and the inferred builder
|
|
8
|
+
* type collapses to `SchemaBuilder<unknown>`. Always pass the schema literal
|
|
9
|
+
* (or the variable holding it) with `as const`.
|
|
10
|
+
*
|
|
11
|
+
* **Supported JSON Schema keywords**
|
|
12
|
+
*
|
|
13
|
+
* | Keyword | Builder equivalent |
|
|
14
|
+
* | ------------------------------ | ------------------------------- |
|
|
15
|
+
* | `type: 'string'` | `string()` |
|
|
16
|
+
* | `type: 'number'` | `number()` |
|
|
17
|
+
* | `type: 'integer'` | `number()` (integer flag) |
|
|
18
|
+
* | `type: 'boolean'` | `boolean()` |
|
|
19
|
+
* | `type: 'null'` | `SchemaBuilder<null>` |
|
|
20
|
+
* | `type: 'array'` + `items` | `array(itemBuilder)` |
|
|
21
|
+
* | `type: 'object'` + `properties`| `object({ … })` |
|
|
22
|
+
* | `required: […]` | required / optional per-prop |
|
|
23
|
+
* | `additionalProperties: true` | `.acceptUnknownProps()` |
|
|
24
|
+
* | `const` | `string/number/boolean eq` |
|
|
25
|
+
* | `enum` | `union(…)` |
|
|
26
|
+
* | `anyOf` | `union(…)` |
|
|
27
|
+
* | `minLength` / `maxLength` | `.minLength()` / `.maxLength()` |
|
|
28
|
+
* | `pattern` | `.matches(regex)` (invalid patterns silently ignored) |
|
|
29
|
+
* | `minimum` / `maximum` | `.min()` / `.max()` |
|
|
30
|
+
* | `exclusiveMinimum` / `exclusiveMaximum` | custom validator (not round-trippable via `toJsonSchema`) |
|
|
31
|
+
* | `multipleOf` | `.multipleOf()` |
|
|
32
|
+
* | `minItems` / `maxItems` | `.minLength()` / `.maxLength()` |
|
|
33
|
+
* | `format: 'email'` | `.email()` extension |
|
|
34
|
+
* | `format: 'uuid'` | `.uuid()` extension |
|
|
35
|
+
* | `format: 'uri'` / `'url'` | `.url()` extension |
|
|
36
|
+
* | `format: 'ipv4'` | `.ip({ version: 'v4' })` |
|
|
37
|
+
* | `format: 'ipv6'` | `.ip({ version: 'v6' })` |
|
|
38
|
+
* | `format: 'date-time'` | `.matches(iso8601 regex)` |
|
|
39
|
+
*
|
|
40
|
+
* Keywords **not** supported: `allOf` (falls back to `any()`), `$ref`,
|
|
41
|
+
* `$defs`, `if/then/else`, `not`, `contains`, `unevaluatedProperties`,
|
|
42
|
+
* `contentEncoding`.
|
|
43
|
+
*
|
|
44
|
+
* @param schema - A JSON Schema literal. Pass with `as const` for precise
|
|
45
|
+
* TypeScript type inference on the returned builder.
|
|
46
|
+
* @returns A `@cleverbrush/schema` builder whose static type mirrors the
|
|
47
|
+
* structure described by the JSON Schema node.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* import { fromJsonSchema } from '@cleverbrush/schema-json';
|
|
52
|
+
*
|
|
53
|
+
* const schema = fromJsonSchema({
|
|
54
|
+
* type: 'object',
|
|
55
|
+
* properties: {
|
|
56
|
+
* name: { type: 'string', minLength: 1 },
|
|
57
|
+
* score: { type: 'number', minimum: 0, maximum: 100 },
|
|
58
|
+
* },
|
|
59
|
+
* required: ['name'],
|
|
60
|
+
* } as const);
|
|
61
|
+
*
|
|
62
|
+
* schema.parse({ name: 'Alice', score: 95 });
|
|
63
|
+
* // TypeScript infers: { name: string; score?: number }
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* // Union types via `enum`
|
|
69
|
+
* const statusSchema = fromJsonSchema({
|
|
70
|
+
* enum: ['active', 'inactive', 'pending']
|
|
71
|
+
* } as const);
|
|
72
|
+
*
|
|
73
|
+
* statusSchema.parse('active'); // valid — 'active' | 'inactive' | 'pending'
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* // Use InferFromJsonSchema to derive the TypeScript type statically
|
|
79
|
+
* import type { InferFromJsonSchema } from '@cleverbrush/schema-json';
|
|
80
|
+
*
|
|
81
|
+
* const S = { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] } as const;
|
|
82
|
+
* type Payload = InferFromJsonSchema<typeof S>; // { id: number }
|
|
83
|
+
* const payloadSchema = fromJsonSchema(S);
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export declare function fromJsonSchema<const S>(schema: S): JsonSchemaNodeToBuilder<S>;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@cleverbrush/schema-json` — Bidirectional JSON Schema interop for `@cleverbrush/schema`.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { toJsonSchema, fromJsonSchema } from '@cleverbrush/schema-json';
|
|
7
|
+
* import { object, string, number } from '@cleverbrush/schema';
|
|
8
|
+
*
|
|
9
|
+
* // Schema → JSON Schema
|
|
10
|
+
* const spec = toJsonSchema(object({ name: string(), age: number().optional() }));
|
|
11
|
+
*
|
|
12
|
+
* // JSON Schema → Schema (use `as const` for type inference)
|
|
13
|
+
* const schema = fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } as const);
|
|
14
|
+
* schema.parse({ name: 'Alice' }); // inferred as { name: string }
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
export type { StandardJSONSchemaV1, StandardTypedV1 } from '@standard-schema/spec';
|
|
20
|
+
export { fromJsonSchema } from './fromJsonSchema.js';
|
|
21
|
+
export { withStandardJsonSchema } from './standardJsonSchema.js';
|
|
22
|
+
export { toJsonSchema } from './toJsonSchema.js';
|
|
23
|
+
export type { InferFromJsonSchema, JsonSchemaNode, JsonSchemaNodeToBuilder, ToJsonSchemaOptions } from './types.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{any as u,array as J,boolean as b,nul as v,number as l,object as B,string as x,tuple as N,union as O}from"@cleverbrush/schema";function p(n){return{valid:!1,errors:[{message:n}]}}function y(){return{valid:!0,errors:[]}}function m(n){if(typeof n!="object"||n===null)return u();let e=n,t;if("enum"in e&&Array.isArray(e.enum))t=j(e.enum);else if("const"in e)t=h(e.const);else if("anyOf"in e&&Array.isArray(e.anyOf))t=I(e.anyOf);else if("allOf"in e&&Array.isArray(e.allOf))t=u();else if(!("type"in e))t=u();else switch(e.type){case"string":t=T(e);break;case"number":t=S(e,!1);break;case"integer":t=S(e,!0);break;case"boolean":t=b();break;case"null":t=v();break;case"array":t=L(e);break;case"object":t=R(e);break;default:t=u()}return e.readOnly===!0&&(t=t.readonly()),typeof e.description=="string"&&e.description&&(t=t.describe(e.description)),t}function h(n){return typeof n=="string"?x(n):typeof n=="number"?l(n):typeof n=="boolean"?b().equals(n):u()}function T(n){let e=x(),t=n.format;if(t==="email"?e=e.email():t==="uuid"?e=e.uuid():t==="uri"||t==="url"?e=e.url():t==="ipv4"?e=e.ip({version:"v4"}):t==="ipv6"?e=e.ip({version:"v6"}):t==="date-time"&&(e=e.addValidator(a=>typeof a=="string"&&!Number.isNaN(Date.parse(a))?y():p("must be a valid date-time string"))),typeof n.minLength=="number"&&(e=e.minLength(n.minLength)),typeof n.maxLength=="number"&&(e=e.maxLength(n.maxLength)),typeof n.pattern=="string")try{e=e.matches(new RegExp(n.pattern))}catch{}return e}function S(n,e){let t=e?l().isInteger():l().isFloat();if(typeof n.minimum=="number"&&(t=t.min(n.minimum)),typeof n.maximum=="number"&&(t=t.max(n.maximum)),typeof n.multipleOf=="number"&&(t=t.multipleOf(n.multipleOf)),typeof n.exclusiveMinimum=="number"){let a=n.exclusiveMinimum;t=t.addValidator(r=>typeof r=="number"&&r>a?y():p(`must be greater than ${a}`))}if(typeof n.exclusiveMaximum=="number"){let a=n.exclusiveMaximum;t=t.addValidator(r=>typeof r=="number"&&r<a?y():p(`must be less than ${a}`))}return t}function L(n){if(Array.isArray(n.prefixItems)){let t=n.prefixItems.map(m),a=N(t);return n.items!==void 0&&n.items!==!1&&typeof n.items=="object"&&(a=a.rest(m(n.items))),a}let e=J();return n.items!==void 0&&(e=e.of(m(n.items))),typeof n.minItems=="number"&&(e=e.minLength(n.minItems)),typeof n.maxItems=="number"&&(e=e.maxLength(n.maxItems)),e}function R(n){let e=n.properties,t=new Set(Array.isArray(n.required)?n.required:[]),a=B();if(e)for(let[r,i]of Object.entries(e)){let s=m(i);a=a.addProp(r,t.has(r)?s.required():s.optional())}return n.additionalProperties!==!1&&(a=a.acceptUnknownProps()),a}function j(n){if(n.length===0)return u();let[e,...t]=n,a=O(h(e));for(let r of t)a=a.or(h(r));return a}function I(n){if(n.length===0)return u();let[e,...t]=n,a=O(m(e));for(let r of t)a=a.or(m(r));return a}function q(n){return m(n)}function w(n){return n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function $(n){let e=n.introspect(),t=e.extensions??{},a=e.isReadonly===!0?{readOnly:!0}:{};switch(e.type){case"string":{if(e.equalsTo!==void 0)return{...a,const:e.equalsTo};let r={...a,type:"string"};if(t.email===!0)r.format="email";else if(t.uuid===!0)r.format="uuid";else if(t.url)r.format="uri";else if(t.ip){let i=typeof t.ip=="object"?t.ip.version:void 0;i==="v4"?r.format="ipv4":i==="v6"&&(r.format="ipv6")}if(e.minLength!==void 0&&(r.minLength=e.minLength),e.maxLength!==void 0&&(r.maxLength=e.maxLength),t.nonempty===!0&&r.minLength===void 0&&(r.minLength=1),e.matches instanceof RegExp){if(e.matches.flags!=="")throw new Error(`Cannot convert RegExp /${e.matches.source}/${e.matches.flags} to JSON Schema pattern: RegExp flags are not representable in standard JSON Schema.`);r.pattern=e.matches.source}else e.startsWith!==void 0?r.pattern=`^${w(e.startsWith)}`:e.endsWith!==void 0&&(r.pattern=`${w(e.endsWith)}$`);return r}case"number":{if(e.equalsTo!==void 0)return{...a,const:e.equalsTo};let r={...a,type:e.isInteger?"integer":"number"};return e.min!==void 0&&(r.minimum=e.min),e.max!==void 0&&(r.maximum=e.max),t.multipleOf!==void 0&&(r.multipleOf=t.multipleOf),t.positive===!0&&(r.exclusiveMinimum=0),t.negative===!0&&(r.exclusiveMaximum=0),r}case"boolean":return e.equalsTo!==void 0?{...a,const:e.equalsTo}:{...a,type:"boolean"};case"date":return{...a,type:"string",format:"date-time"};case"array":{let r={...a,type:"array"};return e.elementSchema&&(r.items=c(e.elementSchema)),e.minLength!==void 0&&(r.minItems=e.minLength),e.maxLength!==void 0&&(r.maxItems=e.maxLength),t.nonempty===!0&&r.minItems===void 0&&(r.minItems=1),r}case"tuple":{let r=e.elements??[],i={type:"array",prefixItems:r.map(c),minItems:r.length};return e.restSchema?i.items=c(e.restSchema):(i.items=!1,i.maxItems=r.length),i}case"object":{let r={...a,type:"object"},i=e.properties;if(i){let s={},f=[];for(let[o,g]of Object.entries(i))s[o]=c(g),g.introspect().isRequired!==!1&&f.push(o);r.properties=s,f.length>0&&(r.required=f)}return e.acceptUnknownProps===!1&&(r.additionalProperties=!1),r}case"null":return{...a,type:"null"};case"union":{let r=e.options??[],i=[],s=r.length>0;for(let f of r){let o=f.introspect();if((o.type==="string"||o.type==="number"||o.type==="boolean")&&o.equalsTo!==void 0)i.push(o.equalsTo);else{s=!1;break}}return s?{...a,enum:i}:{...a,anyOf:r.map(c)}}default:return{}}}function c(n){let e=$(n),t=n.introspect();return typeof t.description=="string"&&t.description!==""&&(e.description=t.description),e}function d(n,e){let t=c(n);return e?.$schema===!1?t:{$schema:(e?.draft??"2020-12")==="07"?"http://json-schema.org/draft-07/schema#":"https://json-schema.org/draft/2020-12/schema",...t}}function k(n){switch(n){case"draft-2020-12":return{draft:"2020-12",$schema:!1};case"draft-07":return{draft:"07",$schema:!1};case"openapi-3.0":return{draft:"07",$schema:!1};default:throw new Error(`@cleverbrush/schema-json: unsupported JSON Schema target "${n}".`)}}function V(n){let e={input(r){return d(n,k(r.target))},output(r){return d(n,k(r.target))}},a={...n["~standard"],jsonSchema:e};return Object.defineProperty(n,"~standard",{value:a,configurable:!0,writable:!1,enumerable:!1}),n}export{q as fromJsonSchema,d as toJsonSchema,V as withStandardJsonSchema};
|
|
2
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fromJsonSchema.ts","../src/toJsonSchema.ts","../src/standardJsonSchema.ts"],"sourcesContent":["import type { SchemaBuilder } from '@cleverbrush/schema';\nimport {\n any,\n array,\n boolean,\n nul,\n number,\n object,\n string,\n tuple,\n union\n} from '@cleverbrush/schema';\nimport type { JsonSchemaNodeToBuilder } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction fail(msg: string) {\n return { valid: false as const, errors: [{ message: msg }] };\n}\n\nfunction ok() {\n return { valid: true as const, errors: [] };\n}\n\nfunction buildNode(s: unknown): SchemaBuilder<any, any, any> {\n if (typeof s !== 'object' || s === null) return any();\n const node = s as Record<string, unknown>;\n\n let b: SchemaBuilder<any, any, any>;\n\n if ('enum' in node && Array.isArray(node['enum']))\n b = buildEnum(node['enum']);\n else if ('const' in node) b = buildConst(node['const']);\n else if ('anyOf' in node && Array.isArray(node['anyOf']))\n b = buildAnyOf(node['anyOf']);\n // allOf is not supported (no intersection builder); fall back to any()\n else if ('allOf' in node && Array.isArray(node['allOf'])) b = any();\n else if (!('type' in node)) b = any();\n else {\n switch (node['type']) {\n case 'string':\n b = buildString(node);\n break;\n case 'number':\n b = buildNumber(node, false);\n break;\n case 'integer':\n b = buildNumber(node, true);\n break;\n case 'boolean':\n b = boolean();\n break;\n case 'null':\n b = nul();\n break;\n case 'array':\n b = buildArray(node);\n break;\n case 'object':\n b = buildObject(node);\n break;\n default:\n b = any();\n }\n }\n\n if (node['readOnly'] === true) b = (b as any).readonly();\n if (typeof node['description'] === 'string' && node['description'])\n b = (b as any).describe(node['description']);\n return b;\n}\n\nfunction buildConst(val: unknown): SchemaBuilder<any, any, any> {\n if (typeof val === 'string') return string(val as any);\n if (typeof val === 'number') return number(val as any);\n if (typeof val === 'boolean') return boolean().equals(val);\n return any();\n}\n\nfunction buildString(\n node: Record<string, unknown>\n): SchemaBuilder<any, any, any> {\n let b: any = string();\n const format = node['format'] as string | undefined;\n\n if (format === 'email') {\n b = b.email();\n } else if (format === 'uuid') {\n b = b.uuid();\n } else if (format === 'uri' || format === 'url') {\n b = b.url();\n } else if (format === 'ipv4') {\n b = b.ip({ version: 'v4' });\n } else if (format === 'ipv6') {\n b = b.ip({ version: 'v6' });\n } else if (format === 'date-time') {\n b = b.addValidator((v: unknown) =>\n typeof v === 'string' && !Number.isNaN(Date.parse(v))\n ? ok()\n : fail('must be a valid date-time string')\n );\n }\n\n if (typeof node['minLength'] === 'number')\n b = b.minLength(node['minLength']);\n if (typeof node['maxLength'] === 'number')\n b = b.maxLength(node['maxLength']);\n if (typeof node['pattern'] === 'string') {\n try {\n b = b.matches(new RegExp(node['pattern']));\n } catch {\n // invalid regex pattern — silently ignore\n }\n }\n return b;\n}\n\nfunction buildNumber(\n node: Record<string, unknown>,\n integer: boolean\n): SchemaBuilder<any, any, any> {\n let b: any = integer ? number().isInteger() : number().isFloat();\n if (typeof node['minimum'] === 'number') b = b.min(node['minimum']);\n if (typeof node['maximum'] === 'number') b = b.max(node['maximum']);\n if (typeof node['multipleOf'] === 'number') {\n b = b.multipleOf(node['multipleOf']);\n }\n if (typeof node['exclusiveMinimum'] === 'number') {\n const min = node['exclusiveMinimum'];\n b = b.addValidator((v: unknown) =>\n typeof v === 'number' && v > min\n ? ok()\n : fail(`must be greater than ${min}`)\n );\n }\n if (typeof node['exclusiveMaximum'] === 'number') {\n const max = node['exclusiveMaximum'];\n b = b.addValidator((v: unknown) =>\n typeof v === 'number' && v < max\n ? ok()\n : fail(`must be less than ${max}`)\n );\n }\n return b;\n}\n\nfunction buildArray(\n node: Record<string, unknown>\n): SchemaBuilder<any, any, any> {\n // JSON Schema 2020-12 tuples use `prefixItems`\n if (Array.isArray(node['prefixItems'])) {\n const elements = (node['prefixItems'] as unknown[]).map(buildNode);\n let b: any = tuple(elements as any);\n // `items: false` means no extra elements (default for tuple)\n // `items: <schema>` means rest elements validated against that schema\n if (\n node['items'] !== undefined &&\n node['items'] !== false &&\n typeof node['items'] === 'object'\n ) {\n b = b.rest(buildNode(node['items']));\n }\n return b;\n }\n let b: any = array();\n if (node['items'] !== undefined) b = b.of(buildNode(node['items']));\n if (typeof node['minItems'] === 'number') b = b.minLength(node['minItems']);\n if (typeof node['maxItems'] === 'number') b = b.maxLength(node['maxItems']);\n return b;\n}\n\nfunction buildObject(\n node: Record<string, unknown>\n): SchemaBuilder<any, any, any> {\n const props = node['properties'] as Record<string, unknown> | undefined;\n const requiredSet = new Set<string>(\n Array.isArray(node['required']) ? (node['required'] as string[]) : []\n );\n let b: any = object();\n if (props) {\n for (const [key, propNode] of Object.entries(props)) {\n const propBuilder = buildNode(propNode);\n b = b.addProp(\n key,\n requiredSet.has(key)\n ? propBuilder.required()\n : propBuilder.optional()\n );\n }\n }\n if (node['additionalProperties'] !== false) b = b.acceptUnknownProps();\n return b;\n}\n\nfunction buildEnum(values: unknown[]): SchemaBuilder<any, any, any> {\n if (values.length === 0) return any();\n const [first, ...rest] = values;\n let b: any = union(buildConst(first));\n for (const v of rest) b = b.or(buildConst(v));\n return b;\n}\n\nfunction buildAnyOf(options: unknown[]): SchemaBuilder<any, any, any> {\n if (options.length === 0) return any();\n const [first, ...rest] = options;\n let b: any = union(buildNode(first));\n for (const opt of rest) b = b.or(buildNode(opt));\n return b;\n}\n\n/**\n * Converts a JSON Schema object into a `@cleverbrush/schema` builder.\n *\n * @remarks\n * **`as const` is required for precise TypeScript inference.** Without it\n * TypeScript widens string literals to `string` and the inferred builder\n * type collapses to `SchemaBuilder<unknown>`. Always pass the schema literal\n * (or the variable holding it) with `as const`.\n *\n * **Supported JSON Schema keywords**\n *\n * | Keyword | Builder equivalent |\n * | ------------------------------ | ------------------------------- |\n * | `type: 'string'` | `string()` |\n * | `type: 'number'` | `number()` |\n * | `type: 'integer'` | `number()` (integer flag) |\n * | `type: 'boolean'` | `boolean()` |\n * | `type: 'null'` | `SchemaBuilder<null>` |\n * | `type: 'array'` + `items` | `array(itemBuilder)` |\n * | `type: 'object'` + `properties`| `object({ … })` |\n * | `required: […]` | required / optional per-prop |\n * | `additionalProperties: true` | `.acceptUnknownProps()` |\n * | `const` | `string/number/boolean eq` |\n * | `enum` | `union(…)` |\n * | `anyOf` | `union(…)` |\n * | `minLength` / `maxLength` | `.minLength()` / `.maxLength()` |\n * | `pattern` | `.matches(regex)` (invalid patterns silently ignored) |\n * | `minimum` / `maximum` | `.min()` / `.max()` |\n * | `exclusiveMinimum` / `exclusiveMaximum` | custom validator (not round-trippable via `toJsonSchema`) |\n * | `multipleOf` | `.multipleOf()` |\n * | `minItems` / `maxItems` | `.minLength()` / `.maxLength()` |\n * | `format: 'email'` | `.email()` extension |\n * | `format: 'uuid'` | `.uuid()` extension |\n * | `format: 'uri'` / `'url'` | `.url()` extension |\n * | `format: 'ipv4'` | `.ip({ version: 'v4' })` |\n * | `format: 'ipv6'` | `.ip({ version: 'v6' })` |\n * | `format: 'date-time'` | `.matches(iso8601 regex)` |\n *\n * Keywords **not** supported: `allOf` (falls back to `any()`), `$ref`,\n * `$defs`, `if/then/else`, `not`, `contains`, `unevaluatedProperties`,\n * `contentEncoding`.\n *\n * @param schema - A JSON Schema literal. Pass with `as const` for precise\n * TypeScript type inference on the returned builder.\n * @returns A `@cleverbrush/schema` builder whose static type mirrors the\n * structure described by the JSON Schema node.\n *\n * @example\n * ```ts\n * import { fromJsonSchema } from '@cleverbrush/schema-json';\n *\n * const schema = fromJsonSchema({\n * type: 'object',\n * properties: {\n * name: { type: 'string', minLength: 1 },\n * score: { type: 'number', minimum: 0, maximum: 100 },\n * },\n * required: ['name'],\n * } as const);\n *\n * schema.parse({ name: 'Alice', score: 95 });\n * // TypeScript infers: { name: string; score?: number }\n * ```\n *\n * @example\n * ```ts\n * // Union types via `enum`\n * const statusSchema = fromJsonSchema({\n * enum: ['active', 'inactive', 'pending']\n * } as const);\n *\n * statusSchema.parse('active'); // valid — 'active' | 'inactive' | 'pending'\n * ```\n *\n * @example\n * ```ts\n * // Use InferFromJsonSchema to derive the TypeScript type statically\n * import type { InferFromJsonSchema } from '@cleverbrush/schema-json';\n *\n * const S = { type: 'object', properties: { id: { type: 'integer' } }, required: ['id'] } as const;\n * type Payload = InferFromJsonSchema<typeof S>; // { id: number }\n * const payloadSchema = fromJsonSchema(S);\n * ```\n */\nexport function fromJsonSchema<const S>(schema: S): JsonSchemaNodeToBuilder<S> {\n return buildNode(schema) as JsonSchemaNodeToBuilder<S>;\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\nimport type { ToJsonSchemaOptions } from './types.js';\n\ntype Out = Record<string, unknown>;\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction convertNodeInner(schema: SchemaBuilder<any, any, any>): Out {\n const info = schema.introspect() as any;\n const ext: Record<string, unknown> = info.extensions ?? {};\n const readOnly: Out = info.isReadonly === true ? { readOnly: true } : {};\n\n switch (info.type) {\n case 'string': {\n if (info.equalsTo !== undefined)\n return { ...readOnly, const: info.equalsTo };\n const out: Out = { ...readOnly, type: 'string' };\n if (ext['email'] === true) {\n out['format'] = 'email';\n } else if (ext['uuid'] === true) {\n out['format'] = 'uuid';\n } else if (ext['url']) {\n out['format'] = 'uri';\n } else if (ext['ip']) {\n const ver =\n typeof ext['ip'] === 'object'\n ? (ext['ip'] as any).version\n : undefined;\n if (ver === 'v4') out['format'] = 'ipv4';\n else if (ver === 'v6') out['format'] = 'ipv6';\n // both versions: no single standard JSON Schema format — omit\n }\n if (info.minLength !== undefined) out['minLength'] = info.minLength;\n if (info.maxLength !== undefined) out['maxLength'] = info.maxLength;\n // .nonempty() sets minLength via extension; if not already set\n if (ext['nonempty'] === true && out['minLength'] === undefined)\n out['minLength'] = 1;\n if (info.matches instanceof RegExp) {\n if (info.matches.flags !== '') {\n throw new Error(\n `Cannot convert RegExp /${info.matches.source}/${info.matches.flags} to JSON Schema pattern: RegExp flags are not representable in standard JSON Schema.`\n );\n }\n out['pattern'] = info.matches.source;\n } else if (info.startsWith !== undefined) {\n out['pattern'] = `^${escapeRegex(info.startsWith)}`;\n } else if (info.endsWith !== undefined) {\n out['pattern'] = `${escapeRegex(info.endsWith)}$`;\n }\n return out;\n }\n\n case 'number': {\n if (info.equalsTo !== undefined)\n return { ...readOnly, const: info.equalsTo };\n const out: Out = {\n ...readOnly,\n type: info.isInteger ? 'integer' : 'number'\n };\n if (info.min !== undefined) out['minimum'] = info.min;\n if (info.max !== undefined) out['maximum'] = info.max;\n if (ext['multipleOf'] !== undefined)\n out['multipleOf'] = ext['multipleOf'];\n if (ext['positive'] === true) out['exclusiveMinimum'] = 0;\n if (ext['negative'] === true) out['exclusiveMaximum'] = 0;\n return out;\n }\n\n case 'boolean': {\n if (info.equalsTo !== undefined)\n return { ...readOnly, const: info.equalsTo };\n return { ...readOnly, type: 'boolean' };\n }\n\n case 'date':\n return { ...readOnly, type: 'string', format: 'date-time' };\n\n case 'array': {\n const out: Out = { ...readOnly, type: 'array' };\n if (info.elementSchema)\n out['items'] = convertNode(info.elementSchema);\n if (info.minLength !== undefined) out['minItems'] = info.minLength;\n if (info.maxLength !== undefined) out['maxItems'] = info.maxLength;\n if (ext['nonempty'] === true && out['minItems'] === undefined)\n out['minItems'] = 1;\n return out;\n }\n\n case 'tuple': {\n const elements: SchemaBuilder<any, any, any>[] =\n info.elements ?? [];\n const out: Out = {\n type: 'array',\n prefixItems: elements.map(convertNode),\n minItems: elements.length\n };\n if (info.restSchema) {\n out['items'] = convertNode(info.restSchema);\n } else {\n out['items'] = false;\n out['maxItems'] = elements.length;\n }\n return out;\n }\n\n case 'object': {\n const out: Out = { ...readOnly, type: 'object' };\n const props = info.properties as\n | Record<string, SchemaBuilder<any, any, any>>\n | undefined;\n if (props) {\n const outProps: Record<string, unknown> = {};\n const required: string[] = [];\n for (const [key, propSchema] of Object.entries(props)) {\n outProps[key] = convertNode(propSchema);\n if ((propSchema.introspect() as any).isRequired !== false)\n required.push(key);\n }\n out['properties'] = outProps;\n if (required.length > 0) out['required'] = required;\n }\n if (info.acceptUnknownProps === false)\n out['additionalProperties'] = false;\n return out;\n }\n\n case 'null':\n return { ...readOnly, type: 'null' };\n\n case 'union': {\n const options: SchemaBuilder<any, any, any>[] = info.options ?? [];\n const enumValues: unknown[] = [];\n let allConst = options.length > 0;\n for (const opt of options) {\n const oi = opt.introspect() as any;\n if (\n (oi.type === 'string' ||\n oi.type === 'number' ||\n oi.type === 'boolean') &&\n oi.equalsTo !== undefined\n ) {\n enumValues.push(oi.equalsTo);\n } else {\n allConst = false;\n break;\n }\n }\n if (allConst) return { ...readOnly, enum: enumValues };\n return { ...readOnly, anyOf: options.map(convertNode) };\n }\n\n default:\n return {};\n }\n}\n\nfunction convertNode(schema: SchemaBuilder<any, any, any>): Out {\n const out = convertNodeInner(schema);\n const info = schema.introspect() as any;\n if (typeof info.description === 'string' && info.description !== '')\n out['description'] = info.description;\n return out;\n}\n\n/**\n * Converts a `@cleverbrush/schema` builder to a JSON Schema object.\n *\n * @remarks\n * **What round-trips cleanly**: all declarative constraints — type, format,\n * minLength/maxLength, minimum/maximum, multipleOf, pattern, required/optional\n * per property, additionalProperties, items, enum/const literals, anyOf/union.\n *\n * **What is silently omitted**:\n * - Custom validators added via `addValidator` (no JSON Schema equivalent)\n * - Preprocessors added via `addPreprocessor`\n * - JSDoc comments on schema properties\n * - `exclusiveMinimum`/`exclusiveMaximum` constraints from `fromJsonSchema`\n * (stored as custom validators — not introspectable; only `positive()`/\n * `negative()` extension-based exclusives are emitted)\n * - IP format with both v4 _and_ v6 allowed simultaneously (no single\n * standard JSON Schema format covers both; the `format` keyword is omitted\n * in that case)\n *\n * By default the output includes a `$schema` header for JSON Schema Draft\n * 2020-12. Pass `{ $schema: false }` when embedding the result in an OpenAPI\n * specification, or `{ draft: '07' }` for Draft 07 compatibility.\n *\n * @param schema - Any `@cleverbrush/schema` builder instance.\n * @param opts - Optional output configuration (see {@link ToJsonSchemaOptions}).\n * @returns A plain JSON-serialisable object representing the schema.\n *\n * @example\n * ```ts\n * import { toJsonSchema } from '@cleverbrush/schema-json';\n * import { object, string, number } from '@cleverbrush/schema';\n *\n * const UserSchema = object({\n * name: string().minLength(1),\n * email: string().email(),\n * age: number().optional(),\n * });\n *\n * const spec = toJsonSchema(UserSchema);\n * // {\n * // \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n * // \"type\": \"object\",\n * // \"properties\": {\n * // \"name\": { \"type\": \"string\", \"minLength\": 1 },\n * // \"email\": { \"type\": \"string\", \"format\": \"email\" },\n * // \"age\": { \"type\": \"number\" }\n * // },\n * // \"required\": [\"name\", \"email\"]\n * // }\n * ```\n *\n * @example\n * ```ts\n * // Embed in an OpenAPI spec (strip the $schema header)\n * toJsonSchema(schema, { $schema: false });\n *\n * // Use JSON Schema Draft 07\n * toJsonSchema(schema, { draft: '07' });\n * ```\n */\nexport function toJsonSchema(\n schema: SchemaBuilder<any, any, any>,\n opts?: ToJsonSchemaOptions\n): Record<string, unknown> {\n const body = convertNode(schema);\n if (opts?.$schema === false) return body;\n const draft = opts?.draft ?? '2020-12';\n const uri =\n draft === '07'\n ? 'http://json-schema.org/draft-07/schema#'\n : 'https://json-schema.org/draft/2020-12/schema';\n return { $schema: uri, ...body };\n}\n","import type { SchemaBuilder } from '@cleverbrush/schema';\nimport type { StandardJSONSchemaV1 } from '@standard-schema/spec';\nimport { toJsonSchema } from './toJsonSchema.js';\n\n/**\n * Maps Standard JSON Schema target identifiers to the options accepted by\n * {@link toJsonSchema}.\n */\nfunction targetToOptions(target: StandardJSONSchemaV1.Target): {\n draft: '2020-12' | '07';\n $schema: false;\n} {\n switch (target) {\n case 'draft-2020-12':\n return { draft: '2020-12', $schema: false };\n case 'draft-07':\n return { draft: '07', $schema: false };\n case 'openapi-3.0':\n // OpenAPI 3.0 is a superset of JSON Schema Draft 04; Draft 07 is\n // the closest we support.\n return { draft: '07', $schema: false };\n default:\n throw new Error(\n `@cleverbrush/schema-json: unsupported JSON Schema target \"${target}\".`\n );\n }\n}\n\n/**\n * Wraps a `@cleverbrush/schema` builder with the\n * [Standard JSON Schema v1](https://standardschema.dev/) interface.\n *\n * The returned schema object's `~standard` property is enriched with a\n * `jsonSchema` converter. Because `@cleverbrush/schema` does not\n * distinguish between input and output types, both `input()` and `output()`\n * produce the same JSON Schema document.\n *\n * **Note:** this mutates the schema instance by overriding the `~standard`\n * property. The returned reference is the same schema object.\n *\n * @example\n * ```ts\n * import { object, string, number } from '@cleverbrush/schema';\n * import { withStandardJsonSchema } from '@cleverbrush/schema-json';\n *\n * const schema = object({ name: string(), age: number().optional() });\n * const wrapped = withStandardJsonSchema(schema);\n *\n * // Access standard JSON Schema properties\n * wrapped['~standard'].jsonSchema.input({ target: 'draft-2020-12' });\n * wrapped['~standard'].jsonSchema.output({ target: 'draft-07' });\n * ```\n *\n * @param schema - Any `@cleverbrush/schema` builder instance.\n * @returns The same schema instance, now also conforming to `StandardJSONSchemaV1`.\n */\nexport function withStandardJsonSchema<\n T extends SchemaBuilder<any, any, any, any, any>\n>(schema: T): T & StandardJSONSchemaV1 {\n const converter: StandardJSONSchemaV1.Converter = {\n input(options: StandardJSONSchemaV1.Options): Record<string, unknown> {\n return toJsonSchema(schema, targetToOptions(options.target));\n },\n output(options: StandardJSONSchemaV1.Options): Record<string, unknown> {\n return toJsonSchema(schema, targetToOptions(options.target));\n }\n };\n\n // Extend the existing `~standard` props (which already include the\n // StandardSchemaV1 validate function from the schema library) with the\n // jsonSchema converter.\n const existingStandard = schema['~standard'];\n const enrichedStandard = { ...existingStandard, jsonSchema: converter };\n\n // Override the prototype getter with an instance-level property.\n // A Proxy wrapper was considered but JavaScript private fields (#field)\n // are bound to the original class instance; when a method is invoked\n // through a Proxy the receiver is the Proxy, not the real instance, so\n // private-field access throws a TypeError. defineProperty avoids this\n // because `this` inside methods still refers to the original schema.\n Object.defineProperty(schema, '~standard', {\n value: enrichedStandard,\n configurable: true,\n writable: false,\n enumerable: false\n });\n\n return schema as T & StandardJSONSchemaV1;\n}\n"],"mappings":"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","names":["any","array","boolean","nul","number","object","string","tuple","union","fail","msg","ok","buildNode","s","node","b","buildEnum","buildConst","buildAnyOf","buildString","buildNumber","buildArray","buildObject","val","format","v","integer","min","max","elements","props","requiredSet","key","propNode","propBuilder","values","first","rest","options","opt","fromJsonSchema","schema","escapeRegex","s","convertNodeInner","schema","info","ext","readOnly","out","ver","convertNode","elements","props","outProps","required","key","propSchema","options","enumValues","allConst","opt","oi","toJsonSchema","opts","body","targetToOptions","target","withStandardJsonSchema","schema","converter","options","toJsonSchema","enrichedStandard"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SchemaBuilder } from '@cleverbrush/schema';
|
|
2
|
+
import type { StandardJSONSchemaV1 } from '@standard-schema/spec';
|
|
3
|
+
/**
|
|
4
|
+
* Wraps a `@cleverbrush/schema` builder with the
|
|
5
|
+
* [Standard JSON Schema v1](https://standardschema.dev/) interface.
|
|
6
|
+
*
|
|
7
|
+
* The returned schema object's `~standard` property is enriched with a
|
|
8
|
+
* `jsonSchema` converter. Because `@cleverbrush/schema` does not
|
|
9
|
+
* distinguish between input and output types, both `input()` and `output()`
|
|
10
|
+
* produce the same JSON Schema document.
|
|
11
|
+
*
|
|
12
|
+
* **Note:** this mutates the schema instance by overriding the `~standard`
|
|
13
|
+
* property. The returned reference is the same schema object.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { object, string, number } from '@cleverbrush/schema';
|
|
18
|
+
* import { withStandardJsonSchema } from '@cleverbrush/schema-json';
|
|
19
|
+
*
|
|
20
|
+
* const schema = object({ name: string(), age: number().optional() });
|
|
21
|
+
* const wrapped = withStandardJsonSchema(schema);
|
|
22
|
+
*
|
|
23
|
+
* // Access standard JSON Schema properties
|
|
24
|
+
* wrapped['~standard'].jsonSchema.input({ target: 'draft-2020-12' });
|
|
25
|
+
* wrapped['~standard'].jsonSchema.output({ target: 'draft-07' });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @param schema - Any `@cleverbrush/schema` builder instance.
|
|
29
|
+
* @returns The same schema instance, now also conforming to `StandardJSONSchemaV1`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function withStandardJsonSchema<T extends SchemaBuilder<any, any, any, any, any>>(schema: T): T & StandardJSONSchemaV1;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { SchemaBuilder } from '@cleverbrush/schema';
|
|
2
|
+
import type { ToJsonSchemaOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Converts a `@cleverbrush/schema` builder to a JSON Schema object.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* **What round-trips cleanly**: all declarative constraints — type, format,
|
|
8
|
+
* minLength/maxLength, minimum/maximum, multipleOf, pattern, required/optional
|
|
9
|
+
* per property, additionalProperties, items, enum/const literals, anyOf/union.
|
|
10
|
+
*
|
|
11
|
+
* **What is silently omitted**:
|
|
12
|
+
* - Custom validators added via `addValidator` (no JSON Schema equivalent)
|
|
13
|
+
* - Preprocessors added via `addPreprocessor`
|
|
14
|
+
* - JSDoc comments on schema properties
|
|
15
|
+
* - `exclusiveMinimum`/`exclusiveMaximum` constraints from `fromJsonSchema`
|
|
16
|
+
* (stored as custom validators — not introspectable; only `positive()`/
|
|
17
|
+
* `negative()` extension-based exclusives are emitted)
|
|
18
|
+
* - IP format with both v4 _and_ v6 allowed simultaneously (no single
|
|
19
|
+
* standard JSON Schema format covers both; the `format` keyword is omitted
|
|
20
|
+
* in that case)
|
|
21
|
+
*
|
|
22
|
+
* By default the output includes a `$schema` header for JSON Schema Draft
|
|
23
|
+
* 2020-12. Pass `{ $schema: false }` when embedding the result in an OpenAPI
|
|
24
|
+
* specification, or `{ draft: '07' }` for Draft 07 compatibility.
|
|
25
|
+
*
|
|
26
|
+
* @param schema - Any `@cleverbrush/schema` builder instance.
|
|
27
|
+
* @param opts - Optional output configuration (see {@link ToJsonSchemaOptions}).
|
|
28
|
+
* @returns A plain JSON-serialisable object representing the schema.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { toJsonSchema } from '@cleverbrush/schema-json';
|
|
33
|
+
* import { object, string, number } from '@cleverbrush/schema';
|
|
34
|
+
*
|
|
35
|
+
* const UserSchema = object({
|
|
36
|
+
* name: string().minLength(1),
|
|
37
|
+
* email: string().email(),
|
|
38
|
+
* age: number().optional(),
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* const spec = toJsonSchema(UserSchema);
|
|
42
|
+
* // {
|
|
43
|
+
* // "$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
44
|
+
* // "type": "object",
|
|
45
|
+
* // "properties": {
|
|
46
|
+
* // "name": { "type": "string", "minLength": 1 },
|
|
47
|
+
* // "email": { "type": "string", "format": "email" },
|
|
48
|
+
* // "age": { "type": "number" }
|
|
49
|
+
* // },
|
|
50
|
+
* // "required": ["name", "email"]
|
|
51
|
+
* // }
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* // Embed in an OpenAPI spec (strip the $schema header)
|
|
57
|
+
* toJsonSchema(schema, { $schema: false });
|
|
58
|
+
*
|
|
59
|
+
* // Use JSON Schema Draft 07
|
|
60
|
+
* toJsonSchema(schema, { draft: '07' });
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function toJsonSchema(schema: SchemaBuilder<any, any, any>, opts?: ToJsonSchemaOptions): Record<string, unknown>;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for `@cleverbrush/schema-json`.
|
|
3
|
+
*
|
|
4
|
+
* @module
|
|
5
|
+
*/
|
|
6
|
+
import type { BooleanSchemaBuilder, ExtendedArray, ExtendedNumber, ExtendedString, ObjectSchemaBuilder, SchemaBuilder, UnionSchemaBuilder } from '@cleverbrush/schema';
|
|
7
|
+
/**
|
|
8
|
+
* Subset of JSON Schema (Draft 7 / 2020-12) accepted as input.
|
|
9
|
+
*
|
|
10
|
+
* For accurate TypeScript type inference via {@link InferFromJsonSchema},
|
|
11
|
+
* pass schema literals with `as const`.
|
|
12
|
+
*/
|
|
13
|
+
export type JsonSchemaNode = {
|
|
14
|
+
readonly type: 'string';
|
|
15
|
+
readonly format?: string;
|
|
16
|
+
readonly minLength?: number;
|
|
17
|
+
readonly maxLength?: number;
|
|
18
|
+
readonly pattern?: string;
|
|
19
|
+
[k: string]: unknown;
|
|
20
|
+
} | {
|
|
21
|
+
readonly type: 'number' | 'integer';
|
|
22
|
+
readonly minimum?: number;
|
|
23
|
+
readonly maximum?: number;
|
|
24
|
+
readonly exclusiveMinimum?: number;
|
|
25
|
+
readonly exclusiveMaximum?: number;
|
|
26
|
+
readonly multipleOf?: number;
|
|
27
|
+
[k: string]: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
readonly type: 'boolean';
|
|
30
|
+
[k: string]: unknown;
|
|
31
|
+
} | {
|
|
32
|
+
readonly type: 'null';
|
|
33
|
+
[k: string]: unknown;
|
|
34
|
+
} | {
|
|
35
|
+
readonly type: 'array';
|
|
36
|
+
readonly items?: JsonSchemaNode;
|
|
37
|
+
readonly minItems?: number;
|
|
38
|
+
readonly maxItems?: number;
|
|
39
|
+
[k: string]: unknown;
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: 'object';
|
|
42
|
+
readonly properties?: Readonly<Record<string, JsonSchemaNode>>;
|
|
43
|
+
readonly required?: readonly string[];
|
|
44
|
+
readonly additionalProperties?: boolean;
|
|
45
|
+
[k: string]: unknown;
|
|
46
|
+
} | {
|
|
47
|
+
readonly const: unknown;
|
|
48
|
+
[k: string]: unknown;
|
|
49
|
+
} | {
|
|
50
|
+
readonly enum: readonly unknown[];
|
|
51
|
+
[k: string]: unknown;
|
|
52
|
+
} | {
|
|
53
|
+
readonly anyOf: readonly JsonSchemaNode[];
|
|
54
|
+
[k: string]: unknown;
|
|
55
|
+
} | {
|
|
56
|
+
readonly allOf: readonly JsonSchemaNode[];
|
|
57
|
+
[k: string]: unknown;
|
|
58
|
+
} | Record<string, never>;
|
|
59
|
+
/**
|
|
60
|
+
* Recursively infers a TypeScript type from a statically-known JSON Schema node.
|
|
61
|
+
*
|
|
62
|
+
* Requires `as const` on the input object for precise inference —
|
|
63
|
+
* without it TypeScript widens string literals to `string` and inference
|
|
64
|
+
* collapses to `unknown`.
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```ts
|
|
68
|
+
* type User = InferFromJsonSchema<typeof UserJsonSchema>;
|
|
69
|
+
* // { name: string; age?: number }
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export type InferFromJsonSchema<S> = S extends {
|
|
73
|
+
readonly const: infer V;
|
|
74
|
+
} ? V : S extends {
|
|
75
|
+
readonly enum: readonly (infer V)[];
|
|
76
|
+
} ? V : S extends {
|
|
77
|
+
readonly type: 'string';
|
|
78
|
+
} ? string : S extends {
|
|
79
|
+
readonly type: 'number' | 'integer';
|
|
80
|
+
} ? number : S extends {
|
|
81
|
+
readonly type: 'boolean';
|
|
82
|
+
} ? boolean : S extends {
|
|
83
|
+
readonly type: 'null';
|
|
84
|
+
} ? null : S extends {
|
|
85
|
+
readonly type: 'array';
|
|
86
|
+
readonly items: infer I;
|
|
87
|
+
} ? InferFromJsonSchema<I>[] : S extends {
|
|
88
|
+
readonly type: 'array';
|
|
89
|
+
} ? unknown[] : S extends {
|
|
90
|
+
readonly type: 'object';
|
|
91
|
+
readonly properties: infer P;
|
|
92
|
+
readonly required: readonly (infer R extends string)[];
|
|
93
|
+
} ? {
|
|
94
|
+
[K in keyof P & string as K extends R ? K : never]: InferFromJsonSchema<P[K]>;
|
|
95
|
+
} & {
|
|
96
|
+
[K in keyof P & string as K extends R ? never : K]?: InferFromJsonSchema<P[K]>;
|
|
97
|
+
} : S extends {
|
|
98
|
+
readonly type: 'object';
|
|
99
|
+
readonly properties: infer P;
|
|
100
|
+
} ? {
|
|
101
|
+
[K in keyof P]?: InferFromJsonSchema<P[K]>;
|
|
102
|
+
} : S extends {
|
|
103
|
+
readonly type: 'object';
|
|
104
|
+
} ? Record<string, unknown> : S extends {
|
|
105
|
+
readonly anyOf: readonly (infer U)[];
|
|
106
|
+
} ? InferFromJsonSchema<U> : S extends {
|
|
107
|
+
readonly allOf: readonly JsonSchemaNode[];
|
|
108
|
+
} ? unknown : unknown;
|
|
109
|
+
/** Options accepted by {@link toJsonSchema}. */
|
|
110
|
+
export type ToJsonSchemaOptions = {
|
|
111
|
+
/**
|
|
112
|
+
* JSON Schema draft version to reference in the `$schema` header.
|
|
113
|
+
* @default '2020-12'
|
|
114
|
+
*/
|
|
115
|
+
draft?: '2020-12' | '07';
|
|
116
|
+
/**
|
|
117
|
+
* Whether to include the `$schema` header in the output.
|
|
118
|
+
* Set to `false` when embedding in an OpenAPI spec.
|
|
119
|
+
* @default true
|
|
120
|
+
*/
|
|
121
|
+
$schema?: boolean;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Required variant: `ExtendedString<T>` (the return type of `string()`).
|
|
125
|
+
* Optional variant: `ReturnType<ExtendedString<T>['optional']>` (the return
|
|
126
|
+
* type of `string().optional()`).
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
129
|
+
type ExtendedStringBuilder<T extends string = string, TRequired extends boolean = true> = TRequired extends true ? ExtendedString<T> : ReturnType<ExtendedString<T>['optional']>;
|
|
130
|
+
/**
|
|
131
|
+
* Required variant: `ExtendedNumber<T>` (the return type of `number()`).
|
|
132
|
+
* Optional variant: `ReturnType<ExtendedNumber<T>['optional']>` (the return
|
|
133
|
+
* type of `number().optional()`).
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
type ExtendedNumberBuilder<T extends number = number, TRequired extends boolean = true> = TRequired extends true ? ExtendedNumber<T> : ReturnType<ExtendedNumber<T>['optional']>;
|
|
137
|
+
/**
|
|
138
|
+
* Required variant: `ExtendedArray<TElement>` (the return type of `array()`).
|
|
139
|
+
* Optional variant: `ReturnType<ExtendedArray<TElement>['optional']>` (the
|
|
140
|
+
* return type of `array().optional()`).
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
type ExtendedArrayBuilder<TElement extends SchemaBuilder<any, any, any> = SchemaBuilder<any, any, any>, TRequired extends boolean = true> = TRequired extends true ? ExtendedArray<TElement> : ReturnType<ExtendedArray<TElement>['optional']>;
|
|
144
|
+
/**
|
|
145
|
+
* Maps a JSON Schema `const` value to the narrowest possible builder type.
|
|
146
|
+
* @internal
|
|
147
|
+
*/
|
|
148
|
+
type ConstToBuilder<V, TRequired extends boolean> = V extends string ? ExtendedStringBuilder<V, TRequired> : V extends number ? ExtendedNumberBuilder<V, TRequired> : V extends boolean ? BooleanSchemaBuilder<V, TRequired> : SchemaBuilder<V, TRequired>;
|
|
149
|
+
/**
|
|
150
|
+
* Recursively maps a readonly tuple of `const`/`enum` values to a tuple of
|
|
151
|
+
* builders, preserving the exact literal types of each element.
|
|
152
|
+
* @internal
|
|
153
|
+
*/
|
|
154
|
+
type EnumTupleToBuilders<T extends readonly unknown[]> = T extends readonly [] ? [] : T extends readonly [infer First, ...infer Rest extends readonly unknown[]] ? [ConstToBuilder<First, true>, ...EnumTupleToBuilders<Rest>] : [SchemaBuilder<any, any, any>];
|
|
155
|
+
/**
|
|
156
|
+
* Recursively maps a readonly tuple of JSON Schema nodes (e.g. an `anyOf`
|
|
157
|
+
* array) to a tuple of builders for use with `UnionSchemaBuilder`.
|
|
158
|
+
* @internal
|
|
159
|
+
*/
|
|
160
|
+
type SchemaNodesTupleToBuilders<T extends readonly unknown[]> = T extends readonly [] ? [] : T extends readonly [
|
|
161
|
+
infer First,
|
|
162
|
+
...infer Rest extends readonly unknown[]
|
|
163
|
+
] ? [
|
|
164
|
+
JsonSchemaNodeToBuilder<First, true>,
|
|
165
|
+
...SchemaNodesTupleToBuilders<Rest>
|
|
166
|
+
] : [SchemaBuilder<any, any, any>];
|
|
167
|
+
/**
|
|
168
|
+
* Maps an object schema's `properties` record to the property-descriptor
|
|
169
|
+
* record that `ObjectSchemaBuilder` expects as its first type parameter.
|
|
170
|
+
*
|
|
171
|
+
* Properties present in the `required` array get `TRequired = true`;
|
|
172
|
+
* all others get `TRequired = false`.
|
|
173
|
+
* @internal
|
|
174
|
+
*/
|
|
175
|
+
type ObjectPropertiesToBuilders<P extends Record<string, unknown>, R extends string> = {
|
|
176
|
+
[K in keyof P as K extends R ? K : never]: JsonSchemaNodeToBuilder<P[K], true>;
|
|
177
|
+
} & {
|
|
178
|
+
[K in keyof P as K extends R ? never : K]: JsonSchemaNodeToBuilder<P[K], false>;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Recursively maps a statically-known JSON Schema node (passed with
|
|
182
|
+
* `as const`) to the exact `@cleverbrush/schema` builder type, including:
|
|
183
|
+
*
|
|
184
|
+
* - `StringSchemaBuilder`, `NumberSchemaBuilder`, `BooleanSchemaBuilder`
|
|
185
|
+
* for primitive types and `const` literals
|
|
186
|
+
* - `ArraySchemaBuilder<ElementBuilder>` with the element builder inferred
|
|
187
|
+
* from `items`
|
|
188
|
+
* - `ObjectSchemaBuilder<PropertyDescriptors>` where each property is a
|
|
189
|
+
* typed builder with `TRequired = true/false` driven by the `required`
|
|
190
|
+
* array
|
|
191
|
+
* - `UnionSchemaBuilder<[...Builders]>` for `enum` and `anyOf`
|
|
192
|
+
*
|
|
193
|
+
* @typeParam S - The JSON Schema node type (inferred from `as const`).
|
|
194
|
+
* @typeParam TRequired - Whether this node represents a required (`true`) or
|
|
195
|
+
* optional (`false`) value in its parent context. Defaults to `true`.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```ts
|
|
199
|
+
* import { fromJsonSchema } from '@cleverbrush/schema-json';
|
|
200
|
+
* import type { JsonSchemaNodeToBuilder } from '@cleverbrush/schema-json';
|
|
201
|
+
*
|
|
202
|
+
* const S = {
|
|
203
|
+
* type: 'object',
|
|
204
|
+
* properties: {
|
|
205
|
+
* name: { type: 'string' },
|
|
206
|
+
* score: { type: 'number' },
|
|
207
|
+
* },
|
|
208
|
+
* required: ['name'],
|
|
209
|
+
* } as const;
|
|
210
|
+
*
|
|
211
|
+
* type Builder = JsonSchemaNodeToBuilder<typeof S>;
|
|
212
|
+
* // ObjectSchemaBuilder<
|
|
213
|
+
* // { name: ExtendedStringBuilder<string, true> } &
|
|
214
|
+
* // { score: ExtendedNumberBuilder<number, false> }
|
|
215
|
+
* // >
|
|
216
|
+
*
|
|
217
|
+
* const schema = fromJsonSchema(S);
|
|
218
|
+
* // TypeScript knows schema is ObjectSchemaBuilder<...>
|
|
219
|
+
* // with full intellisense on .parse(), .addProp(), etc.
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
export type JsonSchemaNodeToBuilder<S, TRequired extends boolean = true> = S extends {
|
|
223
|
+
readonly const: infer V;
|
|
224
|
+
} ? ConstToBuilder<V, TRequired> : S extends {
|
|
225
|
+
readonly enum: infer Vals extends readonly unknown[];
|
|
226
|
+
} ? UnionSchemaBuilder<EnumTupleToBuilders<Vals>, TRequired> : S extends {
|
|
227
|
+
readonly anyOf: infer Opts extends readonly unknown[];
|
|
228
|
+
} ? UnionSchemaBuilder<SchemaNodesTupleToBuilders<Opts>, TRequired> : S extends {
|
|
229
|
+
readonly allOf: infer _Opts extends readonly unknown[];
|
|
230
|
+
} ? SchemaBuilder<unknown, TRequired> : S extends {
|
|
231
|
+
readonly type: 'string';
|
|
232
|
+
} ? ExtendedStringBuilder<string, TRequired> : S extends {
|
|
233
|
+
readonly type: 'number';
|
|
234
|
+
} ? ExtendedNumberBuilder<number, TRequired> : S extends {
|
|
235
|
+
readonly type: 'integer';
|
|
236
|
+
} ? ExtendedNumberBuilder<number, TRequired> : S extends {
|
|
237
|
+
readonly type: 'boolean';
|
|
238
|
+
} ? BooleanSchemaBuilder<boolean, TRequired> : S extends {
|
|
239
|
+
readonly type: 'null';
|
|
240
|
+
} ? SchemaBuilder<null, TRequired> : S extends {
|
|
241
|
+
readonly type: 'array';
|
|
242
|
+
readonly items: infer I;
|
|
243
|
+
} ? ExtendedArrayBuilder<JsonSchemaNodeToBuilder<I, true>, TRequired> : S extends {
|
|
244
|
+
readonly type: 'array';
|
|
245
|
+
} ? ExtendedArrayBuilder<SchemaBuilder<unknown, true>, TRequired> : S extends {
|
|
246
|
+
readonly type: 'object';
|
|
247
|
+
readonly properties: infer P extends Record<string, unknown>;
|
|
248
|
+
readonly required: readonly (infer R extends string)[];
|
|
249
|
+
} ? ObjectSchemaBuilder<ObjectPropertiesToBuilders<P, R>, TRequired> : S extends {
|
|
250
|
+
readonly type: 'object';
|
|
251
|
+
readonly properties: infer P extends Record<string, unknown>;
|
|
252
|
+
} ? ObjectSchemaBuilder<{
|
|
253
|
+
[K in keyof P]: JsonSchemaNodeToBuilder<P[K], false>;
|
|
254
|
+
}, TRequired> : S extends {
|
|
255
|
+
readonly type: 'object';
|
|
256
|
+
} ? ObjectSchemaBuilder<{}, TRequired> : SchemaBuilder<unknown, TRequired>;
|
|
257
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
|
|
3
|
+
"bugs": {
|
|
4
|
+
"url": "https://github.com/cleverbrush/framework/issues",
|
|
5
|
+
"email": "andrew_zol@cleverbrush.com"
|
|
6
|
+
},
|
|
7
|
+
"description": "Bidirectional JSON Schema (Draft 7 / 2020-12) interop for @cleverbrush/schema",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://docs.cleverbrush.com/modules/_cleverbrush_schema-json.html",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"json schema",
|
|
14
|
+
"schema",
|
|
15
|
+
"validation",
|
|
16
|
+
"cleverbrush"
|
|
17
|
+
],
|
|
18
|
+
"license": "BSD 3-Clause",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"name": "@cleverbrush/schema-json",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@cleverbrush/schema": "0.0.0-beta-20260410073748",
|
|
30
|
+
"@standard-schema/spec": "^1.1.0"
|
|
31
|
+
},
|
|
32
|
+
"readme": "https://github.com/cleverbrush/framework/tree/master/libs/schema-json#readme",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "github:cleverbrush/framework"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"watch": "tsc --build --watch",
|
|
39
|
+
"build": "tsup && tsc --project tsconfig.build.json --emitDeclarationOnly",
|
|
40
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo"
|
|
41
|
+
},
|
|
42
|
+
"type": "module",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"version": "0.0.0-beta-20260410073748"
|
|
45
|
+
}
|