Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support JSON Schema as input for a Type #1159

Draft
wants to merge 74 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
2c2bdf1
improve wording of type example
ssalbdivad Jun 5, 2024
a3fd9cd
Merge branch 'main' into v2docs-6
ssalbdivad Jun 6, 2024
01583b0
runtime error messages increase font size
ssalbdivad Jun 6, 2024
9e3e772
rename RegexNode=>PatternNode
ssalbdivad Jun 6, 2024
85e5558
add changeset
ssalbdivad Jun 6, 2024
886e643
bump deps
ssalbdivad Jun 7, 2024
00baf27
fix morph with alias child
ssalbdivad Jun 7, 2024
2a94bcd
add changelogs
ssalbdivad Jun 7, 2024
98533ad
cyclic scope
ssalbdivad Jun 7, 2024
ae9342e
allow overriding aliases
ssalbdivad Jun 7, 2024
a2ed10f
add constraints to validation scope
ssalbdivad Jun 7, 2024
170bc6e
remove constraints type parameter constraints
ssalbdivad Jun 8, 2024
9d0bf28
remove broken cyclic rereferences test
ssalbdivad Jun 8, 2024
8628a77
cleanup anonymous constraint display
ssalbdivad Jun 8, 2024
77562e3
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Jun 12, 2024
b7c38a0
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Jun 14, 2024
0e5b2da
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Jun 24, 2024
cf77faa
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Jun 25, 2024
4c4e61f
Export from ark/schema/shared/traversal.ts so can import TraversalCon…
TizzySaurus Sep 23, 2024
934cd36
Add parsing & types for array JSON Schema (w/ excessively deep type e…
TizzySaurus Sep 23, 2024
d3bc90d
Add parsing & types for number/integer JSON Schema
TizzySaurus Sep 23, 2024
da95a37
Add parsing & types for string JSON Schema
TizzySaurus Sep 23, 2024
4d230c1
Add ArkType Scope representing JSON Schema schemas
TizzySaurus Sep 23, 2024
48a654a
Initialise ark/jsonschema package
TizzySaurus Sep 23, 2024
b343bfe
Add parsing & types for object JSON Schema
TizzySaurus Sep 23, 2024
98e9c03
Add parsing for allOf + anyOf + not + oneOf JSON Schemas
TizzySaurus Sep 23, 2024
6dbdbcd
Add parsing for const + enum JSON Schemas
TizzySaurus Sep 23, 2024
7248b27
Add core parseJsonSchema with parsing and type inference logic
TizzySaurus Sep 23, 2024
79c0a79
Add ark/jsonschema/index.ts entry level to @ark/jsonschema
TizzySaurus Sep 23, 2024
28da503
Add @ark/jsonschema to root package.json devDepencies
TizzySaurus Sep 23, 2024
4127422
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Sep 23, 2024
474a849
Export type Out from ark/type/keywords/inference.ts
TizzySaurus Sep 23, 2024
95e1b40
Fix @ark/jsonschema use of arktype constraint inference
TizzySaurus Sep 23, 2024
2e21553
Preliminary tests for @ark/jsonschema
TizzySaurus Oct 2, 2024
2d945b3
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Oct 2, 2024
94cdef2
Merge branch 'json-schema-to-arktype' of personal.github.com:TizzySau…
TizzySaurus Oct 2, 2024
6903094
Add ts-ignore comment for excessively deep tuple spread
TizzySaurus Oct 2, 2024
d65fa30
Add preliminary README.md for @ark/jsonschema
TizzySaurus Oct 2, 2024
8e97d87
Linting
TizzySaurus Oct 2, 2024
877f36e
Fix example in README.md
TizzySaurus Oct 2, 2024
d59336c
Remove extra double-slash in comment
TizzySaurus Oct 2, 2024
e150832
Remove old TODO
TizzySaurus Oct 2, 2024
f625bc9
Remove old comment
TizzySaurus Oct 2, 2024
dd130e8
Migrate .js imports to .ts imports
TizzySaurus Oct 5, 2024
4598d02
Remove accidentally added ark/jsonschema/del.ts file
TizzySaurus Oct 5, 2024
f7bd853
Remove changeset
TizzySaurus Oct 13, 2024
03ba515
Use type.enumerated and type.unit utils for 'const' and 'enum' JSON S…
TizzySaurus Oct 13, 2024
01e296c
Specify return type of function rather than double casting the return…
TizzySaurus Oct 13, 2024
dc43e17
Make variable assignment clearer & remove debug log statement
TizzySaurus Oct 13, 2024
be4fff2
Merge branch 'json-schema-to-arktype' of github.com:TizzySaurus/arkty…
TizzySaurus Oct 13, 2024
b6e5c70
Update @ark/jsonschema 'scripts' and 'exports' to match new style in …
TizzySaurus Oct 13, 2024
423ba89
Formatting
TizzySaurus Oct 13, 2024
2fe1b7a
Use conflatenateAll util instead of manually filtering out undefined …
TizzySaurus Oct 13, 2024
d0e612b
Remove accidentally added debugging file
TizzySaurus Oct 13, 2024
07a5215
Remove type inference from @ark/jsonschema
TizzySaurus Oct 25, 2024
2da7bb5
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Oct 25, 2024
a1f2911
Fix string tests
TizzySaurus Oct 25, 2024
0e1dac7
Remove redundant duplicate tests
TizzySaurus Oct 25, 2024
fc67ff7
Fix broken types
TizzySaurus Oct 25, 2024
aa81782
Use 'expected' and 'actual' props in ctx.reject and use 'Type' over '…
TizzySaurus Oct 26, 2024
de4c7cc
Use ctx.hasError instead of manually tracking ctx errors
TizzySaurus Oct 26, 2024
ce9b213
Fix incorrect exports from ark/type/index.ts
TizzySaurus Oct 29, 2024
ad9e053
Fix 'Expored variable innerParseJsonSchema has or is using name XXX f…
TizzySaurus Oct 29, 2024
8dc5826
Fix types for innerParseJsonSchema
TizzySaurus Oct 29, 2024
c55e9aa
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Oct 29, 2024
624c0ac
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Nov 17, 2024
9826c00
add getDuplicatesOf array util to @ark/util, bumping the package vers…
TizzySaurus Nov 17, 2024
5272e44
Update JSON Schema object parsing logic to use new getDuplicatesOf ut…
TizzySaurus Nov 17, 2024
aaf7d46
Merge branch 'main' into json-schema-to-arktype
TizzySaurus Dec 3, 2024
6221890
Bump @ark/util version from 0.25.0 to 0.26.0 due to new getDuplicates…
TizzySaurus Dec 3, 2024
20f446b
Add support for JSON Schema 'prefixItems' keyword on arrays
TizzySaurus Dec 3, 2024
0fac361
Add unit tests for new 'prefixItems' support
TizzySaurus Dec 3, 2024
65112a1
Make JSON Schema types shared between arktype and @ark/jsonschema
TizzySaurus Dec 3, 2024
1567862
patch-fix broken 'arktype' and '@ark/util' unit tests
TizzySaurus Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/brave-plums-clap.md
TizzySaurus marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@arktype/schema": patch
---

(see [arktype CHANGELOG](../type/CHANGELOG.md))

### Fix a ParseError compiling certain morphs with cyclic inputs

### Rename RegexNode to PatternNode
13 changes: 13 additions & 0 deletions ark/jsonschema/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# @arktype/jsonschema

## 1.0.0

### Initial Release

Released the initial implementation of the package.

Known limitations:

- No `dependencies` support
- No `if`/`else`/`then` support
- `multipleOf` only supports integers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ssalbdivad are you open to lifting ArkType's restriction on divisors being non-integers? Or, if not/as an interim solution, it seems this could be added as a custom narrow constraint?

Copy link
Member

@ssalbdivad ssalbdivad Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add whatever arbitrary validation you want in a narrow, but the reason we don't support this is because there isn't a straightforward way to test float divisibility without complex + potentially fallible string math.

@TizzySaurus was actually working on an implementation but it doesn't make sense to add a dependency or implement that much custom logic for such a niche case.

For rational values like 0.1, you could testing something like (n: number) => (n * 10) % 1 === 0. Outside that though, things tend to get scary so be careful.

33 changes: 33 additions & 0 deletions ark/jsonschema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# @arktype/jsonschema

## What is it?
@arktype/jsonschema is a package that allows converting from a JSON Schema schema, to an ArkType type. For example:
```js
import { parseJsonSchema } from "@ark/jsonschema"

const t = parseJsonSchema({type: "string", minLength: 5, maxLength: 10})
```
is equivalent to:
```js
import { type } from "arktype"

const t = type("5<=string<=10")
```
This enables easy adoption of ArkType for people who currently have JSON Schema based runtime validation in their codebase.

Where possible, the library also has TypeScript type inference so that the runtime validation remains typesafe. Extending on the above example, this means that the return type of the below `parseString` function would be correctly inferred as `string`:
```ts
const assertIsString = (data: unknown)
return t.assert(data)
```

## Extra Type Safety
If you wish to ensure that your JSON Schema schemas are valid, you can do this too! Simply import the `JsonSchema` namespace type from `@ark/jsonschema`, and use the appropriate member like so:
```ts
import type { JsonSchema } from "@ark/jsonschema"

const schema: JsonSchema.StringSchema = {
type: "string",
minLength: "3" // errors stating that 'minLength' must be a number
}
```
130 changes: 130 additions & 0 deletions ark/jsonschema/__tests__/array.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { attest, contextualize } from "@ark/attest"
import { parseJsonSchema } from "@ark/jsonschema"

// TODO: Add compound tests for arrays (e.g. maxItems AND minItems )
// TODO: Add explicit test for negative length constraint failing (since explicitly mentioned in spec)

contextualize(() => {
it("type array", () => {
const t = parseJsonSchema({ type: "array" })
attest<unknown[]>(t.infer)
attest(t.json).snap({ proto: "Array" })
})

it("items & additionalItems", () => {
const tItems = parseJsonSchema({
type: "array",
items: [{ type: "string" }, { type: "number" }]
})
attest<[string, number]>(tItems.infer)
attest(tItems.json).snap({
proto: "Array",
sequence: { prefix: ["string", "number"] },
exactLength: 2
})
attest(tItems.allows(["foo", 1])).equals(true)
attest(tItems.allows([1, "foo"])).equals(false)
attest(tItems.allows(["foo", 1, true])).equals(false)

const tItemsVariadic = parseJsonSchema({
type: "array",
items: [{ type: "string" }, { type: "number" }],
additionalItems: { type: "boolean" }
})
attest<[string, number, ...boolean[]]>(tItemsVariadic.infer)
attest(tItemsVariadic.json).snap({
minLength: 2,
proto: "Array",
sequence: {
prefix: ["string", "number"],
variadic: [{ unit: false }, { unit: true }]
}
})
attest(tItemsVariadic.allows(["foo", 1])).equals(true)
attest(tItemsVariadic.allows([1, "foo", true])).equals(false)
attest(tItemsVariadic.allows([false, "foo", 1])).equals(false)
attest(tItemsVariadic.allows(["foo", 1, true])).equals(true)
})

it("contains", () => {
const tContains = parseJsonSchema({
type: "array",
contains: { type: "number" }
})
const predicateRef =
tContains.internal.firstReferenceOfKindOrThrow(
"predicate"
).serializedPredicate
attest<unknown[]>(tContains.infer)
attest(tContains.json).snap({
proto: "Array",
predicate: [predicateRef]
})
attest(tContains.allows([])).equals(false)
attest(tContains.allows([1, 2, 3])).equals(true)
attest(tContains.allows(["foo", "bar", "baz"])).equals(false)
})

it("maxItems", () => {
const tMaxItems = parseJsonSchema({
type: "array",
maxItems: 5
})
attest<unknown[]>(tMaxItems.infer)
attest(tMaxItems.json).snap({
proto: "Array",
maxLength: 5
})

attest(() => parseJsonSchema({ type: "array", maxItems: -1 })).throws(
"maxItems must be an integer >= 0"
)
})

it("minItems", () => {
const tMinItems = parseJsonSchema({
type: "array",
minItems: 5
})
attest<unknown[]>(tMinItems.infer)
attest(tMinItems.json).snap({
proto: "Array",
minLength: 5
})

attest(() => parseJsonSchema({ type: "array", minItems: -1 })).throws(
"minItems must be an integer >= 0"
)
})

it("uniqueItems", () => {
const tUniqueItems = parseJsonSchema({
type: "array",
uniqueItems: true
})
const predicateRef =
tUniqueItems.internal.firstReferenceOfKindOrThrow(
"predicate"
).serializedPredicate
attest<unknown[]>(tUniqueItems.infer)
attest(tUniqueItems.json).snap({
proto: "Array",
predicate: [predicateRef]
})
attest(tUniqueItems.allows([1, 2, 3])).equals(true)
attest(tUniqueItems.allows([1, 1, 2])).equals(false)
attest(
tUniqueItems.allows([
{ foo: { bar: ["baz", { qux: "quux" }] } },
{ foo: { bar: ["baz", { qux: "quux" }] } }
])
).equals(false)
attest(
// JSON Schema specifies that arrays must be same order to be classified as equal
tUniqueItems.allows([
{ foo: { bar: ["baz", { qux: "quux" }] } },
{ foo: { bar: [{ qux: "quux" }, "baz"] } }
])
).equals(true)
})
})
88 changes: 88 additions & 0 deletions ark/jsonschema/__tests__/json.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { attest, contextualize } from "@ark/attest"
import { parseJsonSchema } from "@ark/jsonschema"
import type { applyConstraintSchema, number } from "arktype"

contextualize(() => {
it("array", () => {
// unknown[]
const parsedJsonSchemaArray = parseJsonSchema({ type: "array" } as const)
attest<unknown[]>(parsedJsonSchemaArray.infer)
attest(parsedJsonSchemaArray.json).snap({ proto: "Array" })

// number[]
const parsedJsonSchemaArrayVariadic = parseJsonSchema({
type: "array",
items: { type: "number", minimum: 3 }
} as const)
attest<number[]>(parsedJsonSchemaArrayVariadic.infer)
attest(parsedJsonSchemaArrayVariadic.json).snap({
proto: "Array",
sequence: { domain: "number", min: 3 }
})
attest<number.atLeast<3>[]>(parsedJsonSchemaArrayVariadic.inferBrandableOut)

// [string]
const parsedJsonSchemaArrayFixed = parseJsonSchema({
type: "array",
items: [{ type: "string" }]
} as const)
attest<[string]>(parsedJsonSchemaArrayFixed.infer)
attest(parsedJsonSchemaArrayFixed.json).snap({
exactLength: 1,
proto: "Array",
sequence: { prefix: ["string"] }
})

// [string, ...number[]]
const parsedJsonSchemaArrayFixedWithVariadic = parseJsonSchema({
type: "array",
items: [{ type: "string" }],
additionalItems: { type: "number" }
} as const)
attest<[string, ...number[]]>(parsedJsonSchemaArrayFixedWithVariadic.infer)

// Maximum Length
const parsedJsonSchemaArrayMaxLength = parseJsonSchema({
type: "array",
items: { type: "string" },
maxItems: 5
} as const)
attest<string[]>(parsedJsonSchemaArrayMaxLength.infer)
attest<applyConstraintSchema<string[], "maxLength", 5>>(
parsedJsonSchemaArrayMaxLength.tOut
)

// Minimum Length
const parsedJsonSchemaArrayMinLength = parseJsonSchema({
type: "array",
items: { type: "number" },
minItems: 3
} as const)
attest<number[]>(parsedJsonSchemaArrayMinLength.infer)
attest<applyConstraintSchema<number[], "minLength", 3>>(
parsedJsonSchemaArrayMinLength.tOut
)

// Maximum & Minimum Length
const parsedJsonSchemaArrayMaxAndMinLength = parseJsonSchema({
type: "array",
items: { type: "array", items: { type: "string" } },
maxItems: 5,
minItems: 3
} as const)
attest<string[][]>(parsedJsonSchemaArrayMaxAndMinLength.infer)
attest<
applyConstraintSchema<
applyConstraintSchema<string[][], "maxLength", 5>,
"minLength",
3
>
>(parsedJsonSchemaArrayMaxAndMinLength.tOut)
})

it("number", () => {})

it("object", () => {})

it("string", () => {})
})
97 changes: 97 additions & 0 deletions ark/jsonschema/__tests__/number.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { attest, contextualize } from "@ark/attest"
import { parseJsonSchema } from "@ark/jsonschema"

// TODO: Compound tests for number (e.g. 'minimum' AND 'maximum')

contextualize(() => {
it("type number", () => {
const jsonSchema = { type: "number" } as const
const expectedArkTypeSchema = { domain: "number" } as const

const parsedNumberValidator = parseJsonSchema(jsonSchema)
attest<number>(parsedNumberValidator.infer)
attest(parsedNumberValidator.json).snap(expectedArkTypeSchema)
})

it("type integer", () => {
const t = parseJsonSchema({ type: "integer" })
attest<number>(t.infer)
attest(t.json).snap({ domain: "number", divisor: 1 })
})

it("maximum & exclusiveMaximum", () => {
const tMax = parseJsonSchema({
type: "number",
maximum: 5
})
attest<number>(tMax.infer)
attest(tMax.json).snap({
domain: "number",
max: 5
})

const tExclMax = parseJsonSchema({
type: "number",
exclusiveMaximum: 5
})
attest<number>(tExclMax.infer)
attest(tExclMax.json).snap({
domain: "number",
max: { rule: 5, exclusive: true }
})

attest(() =>
parseJsonSchema({
type: "number",
maximum: 5,
exclusiveMaximum: 5
})
).throws(
"ParseError: Provided number JSON Schema cannot have 'maximum' and 'exclusiveMaximum"
)
})

it("minimum & exclusiveMinimum", () => {
const tMin = parseJsonSchema({ type: "number", minimum: 5 })
attest<number>(tMin.infer)
attest(tMin.json).snap({ domain: "number", min: 5 })

const tExclMin = parseJsonSchema({
type: "number",
exclusiveMinimum: 5
})
attest<number>(tExclMin.infer)
attest(tExclMin.json).snap({
domain: "number",
min: { rule: 5, exclusive: true }
})

attest(() =>
parseJsonSchema({
type: "number",
minimum: 5,
exclusiveMinimum: 5
})
).throws(
"ParseError: Provided number JSON Schema cannot have 'minimum' and 'exclusiveMinimum"
)
})

it("multipleOf", () => {
const t = parseJsonSchema({ type: "number", multipleOf: 5 })
attest<number>(t.infer)
attest(t.json).snap({ domain: "number", divisor: 5 })

const tInt = parseJsonSchema({
type: "integer",
multipleOf: 5
})
attest<number>(tInt.infer)
attest(tInt.json).snap({ domain: "number", divisor: 5 })

// JSON Schema allows decimal multipleOf, but ArkType doesn't.
attest(() => parseJsonSchema({ type: "number", multipleOf: 5.5 })).throws(
"AggregateError: multipleOf must be an integer"
)
})
})
Loading
Loading