diff --git a/ark/jsonschema/CHANGELOG.md b/ark/jsonschema/CHANGELOG.md
new file mode 100644
index 0000000000..4f4fb7479e
--- /dev/null
+++ b/ark/jsonschema/CHANGELOG.md
@@ -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
diff --git a/ark/jsonschema/README.md b/ark/jsonschema/README.md
new file mode 100644
index 0000000000..a89bc13962
--- /dev/null
+++ b/ark/jsonschema/README.md
@@ -0,0 +1,51 @@
+# @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 relevant `Schema` type from `@ark/jsonschema` like so:
+
+```ts
+import type { JsonSchema } from "arktype"
+
+const integerSchema: JsonSchema.Numeric = {
+	type: "integer",
+	multipleOf: "3" // errors stating that 'multipleOf' must be a number
+}
+```
+
+Note that for string schemas exclusively, you must import the schema type from `@ark/jsonschema` instead of `arktype`. This is because `@ark/jsonschema` doesn't yet support the `format` keyword whilst `arktype` does.
+
+```ts
+import type { StringSchema } from "@ark/jsonschema"
+const stringSchema: StringSchema = {
+	type: "string",
+	minLength: "3" // errors stating that 'minLength' must be a number
+}
+```
diff --git a/ark/jsonschema/__tests__/array.test.ts b/ark/jsonschema/__tests__/array.test.ts
new file mode 100644
index 0000000000..07c16ff134
--- /dev/null
+++ b/ark/jsonschema/__tests__/array.test.ts
@@ -0,0 +1,141 @@
+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(t.json).snap({ proto: "Array" })
+	})
+
+	it("items & prefixItems", () => {
+		const tItems = parseJsonSchema({ type: "array", items: { type: "string" } })
+		attest(tItems.json).snap({ proto: "Array", sequence: "string" })
+		attest(tItems.allows(["foo"])).equals(true)
+		attest(tItems.allows(["foo", "bar"])).equals(true)
+		attest(tItems.allows(["foo", 3, "bar"])).equals(false)
+
+		const tItemsArr = parseJsonSchema({
+			type: "array",
+			items: [{ type: "string" }, { type: "number" }]
+		})
+		attest(tItemsArr.json).snap({
+			proto: "Array",
+			sequence: { prefix: ["string", "number"] },
+			exactLength: 2
+		})
+		attest(tItemsArr.allows(["foo", 1])).equals(true)
+		attest(tItemsArr.allows([1, "foo"])).equals(false)
+		attest(tItemsArr.allows(["foo", 1, true])).equals(false)
+
+		const tPrefixItems = parseJsonSchema({
+			type: "array",
+			prefixItems: [{ type: "string" }, { type: "number" }]
+		})
+		attest(tPrefixItems.json).snap({
+			proto: "Array",
+			sequence: { prefix: ["string", "number"] },
+			exactLength: 2
+		})
+	})
+
+	it("additionalItems", () => {
+		const tItemsVariadic = parseJsonSchema({
+			type: "array",
+			items: [{ type: "string" }, { type: "number" }],
+			additionalItems: { type: "boolean" }
+		})
+		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(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(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(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(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)
+	})
+})
diff --git a/ark/jsonschema/__tests__/number.test.ts b/ark/jsonschema/__tests__/number.test.ts
new file mode 100644
index 0000000000..8d3b64a2f7
--- /dev/null
+++ b/ark/jsonschema/__tests__/number.test.ts
@@ -0,0 +1,89 @@
+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(parsedNumberValidator.json).snap(expectedArkTypeSchema)
+	})
+
+	it("type integer", () => {
+		const t = parseJsonSchema({ type: "integer" })
+		attest(t.json).snap({ domain: "number", divisor: 1 })
+	})
+
+	it("maximum & exclusiveMaximum", () => {
+		const tMax = parseJsonSchema({
+			type: "number",
+			maximum: 5
+		})
+		attest(tMax.json).snap({
+			domain: "number",
+			max: 5
+		})
+
+		const tExclMax = parseJsonSchema({
+			type: "number",
+			exclusiveMaximum: 5
+		})
+		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(tMin.json).snap({ domain: "number", min: 5 })
+
+		const tExclMin = parseJsonSchema({
+			type: "number",
+			exclusiveMinimum: 5
+		})
+		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(t.json).snap({ domain: "number", divisor: 5 })
+
+		const tInt = parseJsonSchema({
+			type: "integer",
+			multipleOf: 5
+		})
+		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"
+		)
+	})
+})
diff --git a/ark/jsonschema/__tests__/object.test.ts b/ark/jsonschema/__tests__/object.test.ts
new file mode 100644
index 0000000000..ff20b7f171
--- /dev/null
+++ b/ark/jsonschema/__tests__/object.test.ts
@@ -0,0 +1,103 @@
+import { attest, contextualize } from "@ark/attest"
+import { parseJsonSchema } from "@ark/jsonschema"
+
+// TODO: Add compound tests for objects (e.g. 'maxProperties' AND 'minProperties')
+
+contextualize(() => {
+	it("type object", () => {
+		const t = parseJsonSchema({ type: "object" })
+		attest(t.json).snap({ domain: "object" })
+	})
+
+	it("maxProperties", () => {
+		const tMaxProperties = parseJsonSchema({
+			type: "object",
+			maxProperties: 1
+		})
+		attest(tMaxProperties.json).snap({ domain: "object" })
+		attest(tMaxProperties.allows({})).equals(true)
+		attest(tMaxProperties.allows({ foo: 1 })).equals(true)
+		attest(tMaxProperties.allows({ foo: 1, bar: 2 })).equals(false)
+		attest(tMaxProperties.allows({ foo: 1, bar: 2, baz: 3 })).equals(false)
+	})
+
+	it("minProperties", () => {
+		const tMinProperties = parseJsonSchema({
+			type: "object",
+			minProperties: 2
+		})
+		attest(tMinProperties.json).snap({ domain: "object" })
+		attest(tMinProperties.allows({})).equals(false)
+		attest(tMinProperties.allows({ foo: 1 })).equals(false)
+		attest(tMinProperties.allows({ foo: 1, bar: 2 })).equals(true)
+		attest(tMinProperties.allows({ foo: 1, bar: 2, baz: 3 })).equals(true)
+	})
+
+	it("properties & required", () => {
+		const tRequired = parseJsonSchema({
+			type: "object",
+			properties: {
+				foo: { type: "string" },
+				bar: { type: "number" }
+			},
+			required: ["foo"]
+		})
+		attest(tRequired.json).snap({
+			domain: "object",
+			required: [{ key: "foo", value: "string" }],
+			optional: [{ key: "bar", value: "number" }]
+		})
+
+		attest(() => parseJsonSchema({ type: "object", required: ["foo"] })).throws(
+			"'required' array is present but 'properties' object is missing"
+		)
+		attest(() =>
+			parseJsonSchema({
+				type: "object",
+				properties: { foo: { type: "string" } },
+				required: ["bar"]
+			})
+		).throws(
+			"Key 'bar' in 'required' array is not present in 'properties' object"
+		)
+		attest(() =>
+			parseJsonSchema({
+				type: "object",
+				properties: { foo: { type: "string" } },
+				required: ["foo", "foo"]
+			})
+		).throws("Duplicate keys in 'required' array")
+	})
+
+	it("additionalProperties", () => {
+		const tAdditionalProperties = parseJsonSchema({
+			type: "object",
+			additionalProperties: { type: "number" }
+		})
+		attest(tAdditionalProperties.json).snap({
+			domain: "object",
+			additional: "number"
+		})
+		attest(tAdditionalProperties.allows({})).equals(true)
+		attest(tAdditionalProperties.allows({ foo: 1 })).equals(true)
+		attest(tAdditionalProperties.allows({ foo: 1, bar: 2 })).equals(true)
+		attest(tAdditionalProperties.allows({ foo: 1, bar: "2" })).equals(false)
+	})
+
+	it("patternProperties", () => {
+		const tPatternProperties = parseJsonSchema({
+			type: "object",
+			patternProperties: {
+				"^[a-z]+$": { type: "string" }
+			}
+		})
+		attest(tPatternProperties.json).snap({
+			domain: "object",
+			pattern: [{ key: "^[a-z]+$", value: "string" }]
+		})
+		attest(tPatternProperties.allows({})).equals(true)
+		attest(tPatternProperties.allows({ foo: "bar" })).equals(true)
+		attest(tPatternProperties.allows({ foo: 1 })).equals(false)
+		attest(tPatternProperties.allows({ "123": "bar" })).equals(false)
+	})
+})
diff --git a/ark/jsonschema/__tests__/string.test.ts b/ark/jsonschema/__tests__/string.test.ts
new file mode 100644
index 0000000000..00e00ac9d2
--- /dev/null
+++ b/ark/jsonschema/__tests__/string.test.ts
@@ -0,0 +1,58 @@
+import { attest, contextualize } from "@ark/attest"
+import { parseJsonSchema } from "@ark/jsonschema"
+
+// TODO: Add compound tests for strings (e.g. maxLength AND pattern)
+// TODO: Add explicit test for negative length constraint failing (since explicitly mentioned in spec)
+
+contextualize(() => {
+	it("type string", () => {
+		const t = parseJsonSchema({ type: "string" })
+		attest(t.json).snap({ domain: "string" })
+	})
+
+	it("maxLength", () => {
+		const tMaxLength = parseJsonSchema({
+			type: "string",
+			maxLength: 5
+		})
+		attest(tMaxLength.json).snap({
+			domain: "string",
+			maxLength: 5
+		})
+	})
+
+	it("minLength", () => {
+		const tMinLength = parseJsonSchema({
+			type: "string",
+			minLength: 5
+		})
+		attest(tMinLength.json).snap({
+			domain: "string",
+			minLength: 5
+		})
+	})
+
+	it("pattern", () => {
+		const tPatternString = parseJsonSchema({
+			type: "string",
+			pattern: "es"
+		})
+		attest(tPatternString.json).snap({
+			domain: "string",
+			pattern: ["es"]
+		})
+		// JSON Schema explicitly specifies that regexes MUST NOT be implicitly anchored
+		// https://json-schema.org/draft-07/draft-handrews-json-schema-validation-01#rfc.section.4.3
+		attest(tPatternString.allows("expression")).equals(true)
+
+		const tPatternRegExp = parseJsonSchema({
+			type: "string",
+			pattern: /es/
+		})
+		attest(tPatternRegExp.json).snap({
+			domain: "string",
+			pattern: ["es"] // strips the outer slashes
+		})
+		attest(tPatternRegExp.allows("expression")).equals(true)
+	})
+})
diff --git a/ark/jsonschema/any.ts b/ark/jsonschema/any.ts
new file mode 100644
index 0000000000..5e45be6771
--- /dev/null
+++ b/ark/jsonschema/any.ts
@@ -0,0 +1,17 @@
+import { throwParseError } from "@ark/util"
+import { type JsonSchema, type Type, type } from "arktype"
+
+export const parseJsonSchemaAnyKeywords = (
+	jsonSchema: JsonSchema
+): Type | undefined => {
+	if ("const" in jsonSchema) {
+		if ("enum" in jsonSchema) {
+			throwParseError(
+				"Provided JSON Schema cannot have both 'const' and 'enum' keywords."
+			)
+		}
+		return type.unit(jsonSchema.const)
+	}
+
+	if ("enum" in jsonSchema) return type.enumerated(jsonSchema.enum)
+}
diff --git a/ark/jsonschema/array.ts b/ark/jsonschema/array.ts
new file mode 100644
index 0000000000..775fe8b642
--- /dev/null
+++ b/ark/jsonschema/array.ts
@@ -0,0 +1,115 @@
+import {
+	rootSchema,
+	type Intersection,
+	type Predicate,
+	type TraversalContext
+} from "@ark/schema"
+import { printable } from "@ark/util"
+import type { Out, Type } from "arktype"
+
+import { parseJsonSchema } from "./json.ts"
+import { JsonSchemaScope, type ArraySchema } from "./scope.ts"
+
+const deepNormalize = (data: unknown): unknown =>
+	typeof data === "object" ?
+		data === null ? null
+		: Array.isArray(data) ? data.map(item => deepNormalize(item))
+		: Object.fromEntries(
+				Object.entries(data)
+					.map(([k, v]) => [k, deepNormalize(v)] as const)
+					.sort((l, r) => (l[0] > r[0] ? 1 : -1))
+			)
+	:	data
+
+const arrayItemsAreUnique = (
+	array: readonly unknown[],
+	ctx: TraversalContext
+) => {
+	const seen: Record<string, true> = {}
+	const duplicates: unknown[] = []
+	for (const item of array) {
+		const stringified = JSON.stringify(deepNormalize(item))
+		if (stringified in seen) duplicates.push(item)
+		else seen[stringified] = true
+	}
+	return duplicates.length === 0 ?
+			true
+		:	ctx.reject({
+				expected: "unique array items",
+				actual: `duplicated at elements ${printable(duplicates)}`
+			})
+}
+
+const arrayContainsItemMatchingSchema = (
+	array: readonly unknown[],
+	schema: Type,
+	ctx: TraversalContext
+) =>
+	array.some(item => schema.allows(item)) === true ?
+		true
+	:	ctx.reject({
+			expected: `an array containing at least one item matching 'contains' schema of ${schema.description}`,
+			actual: printable(array)
+		})
+
+export const validateJsonSchemaArray: Type<
+	(In: ArraySchema) => Out<Type<unknown[], {}>>,
+	any
+> = JsonSchemaScope.ArraySchema.pipe((jsonSchema, ctx) => {
+	const arktypeArraySchema: Intersection.Schema<Array<unknown>> = {
+		proto: "Array"
+	}
+
+	if ("prefixItems" in jsonSchema) {
+		if ("items" in jsonSchema) {
+			ctx.reject({
+				expected: "a valid array JSON Schema",
+				actual:
+					"an array JSON Schema with mutually exclusive keys 'prefixItems' and 'items' specified"
+			})
+		} else jsonSchema.items = jsonSchema.prefixItems
+	}
+
+	if ("items" in jsonSchema) {
+		if (Array.isArray(jsonSchema.items)) {
+			arktypeArraySchema.sequence = {
+				prefix: jsonSchema.items.map(item => parseJsonSchema(item).internal)
+			}
+
+			if ("additionalItems" in jsonSchema) {
+				if (jsonSchema.additionalItems === false)
+					arktypeArraySchema.exactLength = jsonSchema.items.length
+				else {
+					arktypeArraySchema.sequence = {
+						...arktypeArraySchema.sequence,
+						variadic: parseJsonSchema(jsonSchema.additionalItems).internal
+					}
+				}
+			}
+		} else {
+			arktypeArraySchema.sequence = {
+				variadic: parseJsonSchema(jsonSchema.items).json
+			}
+		}
+	}
+
+	if ("maxItems" in jsonSchema)
+		arktypeArraySchema.maxLength = jsonSchema.maxItems
+	if ("minItems" in jsonSchema)
+		arktypeArraySchema.minLength = jsonSchema.minItems
+
+	const predicates: Predicate.Schema[] = []
+	if ("uniqueItems" in jsonSchema && jsonSchema.uniqueItems === true)
+		predicates.push((arr: unknown[], ctx) => arrayItemsAreUnique(arr, ctx))
+
+	if ("contains" in jsonSchema) {
+		const parsedContainsJsonSchema = parseJsonSchema(jsonSchema.contains)
+		predicates.push((arr: unknown[], ctx) =>
+			arrayContainsItemMatchingSchema(arr, parsedContainsJsonSchema, ctx)
+		)
+	}
+
+	arktypeArraySchema.predicate = predicates
+
+	return rootSchema(arktypeArraySchema) as never
+})
diff --git a/ark/jsonschema/composition.ts b/ark/jsonschema/composition.ts
new file mode 100644
index 0000000000..7ca32134d8
--- /dev/null
+++ b/ark/jsonschema/composition.ts
@@ -0,0 +1,66 @@
+import { printable } from "@ark/util"
+import { type, type JsonSchema, type Type } from "arktype"
+import { parseJsonSchema } from "./json.ts"
+
+const validateAllOfJsonSchemas = (jsonSchemas: readonly JsonSchema[]): Type =>
+	jsonSchemas
+		.map(jsonSchema => parseJsonSchema(jsonSchema))
+		.reduce((acc, validator) => acc.and(validator))
+
+const validateAnyOfJsonSchemas = (jsonSchemas: readonly JsonSchema[]): Type =>
+	jsonSchemas
+		.map(jsonSchema => parseJsonSchema(jsonSchema))
+		.reduce((acc, validator) => acc.or(validator))
+
+const validateNotJsonSchema = (jsonSchema: JsonSchema) => {
+	const inner = parseJsonSchema(jsonSchema)
+	return type("unknown").narrow((data, ctx) =>
+		inner.allows(data) ?
+			ctx.reject({
+				expected: `a value that's not ${inner.description}`,
+				actual: printable(data)
+			})
+		:	true
+	) as Type
+}
+
+const validateOneOfJsonSchemas = (jsonSchemas: readonly JsonSchema[]) => {
+	const oneOfValidators = jsonSchemas.map(nestedSchema =>
+		parseJsonSchema(nestedSchema)
+	)
+	const oneOfValidatorsDescriptions = oneOfValidators.map(
+		validator => `○ ${validator.description}`
+	)
+	return (
+		(
+			type("unknown").narrow((data, ctx) => {
+				let matchedValidator: Type | undefined = undefined
+
+				for (const validator of oneOfValidators) {
+					if (validator.allows(data)) {
+						if (matchedValidator === undefined) {
+							matchedValidator = validator
+							continue
+						}
+						return ctx.reject({
+							expected: `exactly one of:\n${oneOfValidatorsDescriptions.join("\n")}`,
+							actual: printable(data)
+						})
+					}
+				}
+				return matchedValidator !== undefined
+			}) as Type
+		)
+			// TODO: Theoretically this shouldn't be necessary due to above `ctx.rejects` in narrow???
+			.describe(`one of:\n${oneOfValidatorsDescriptions.join("\n")}\n`)
+	)
+}
+
+export const parseJsonSchemaCompositionKeywords = (
+	jsonSchema: JsonSchema
+): Type | undefined => {
+	if ("allOf" in jsonSchema) return validateAllOfJsonSchemas(jsonSchema.allOf)
+	if ("anyOf" in jsonSchema) return validateAnyOfJsonSchemas(jsonSchema.anyOf)
+	if ("not" in jsonSchema) return validateNotJsonSchema(jsonSchema.not)
+	if ("oneOf" in jsonSchema) return validateOneOfJsonSchemas(jsonSchema.oneOf)
+}
diff --git a/ark/jsonschema/index.ts b/ark/jsonschema/index.ts
new file mode 100644
index 0000000000..7363a05a1c
--- /dev/null
+++ b/ark/jsonschema/index.ts
@@ -0,0 +1,2 @@
+export { parseJsonSchema } from "./json.ts"
+export * from "./scope.ts"
diff --git a/ark/jsonschema/json.ts b/ark/jsonschema/json.ts
new file mode 100644
index 0000000000..7b15a88f1c
--- /dev/null
+++ b/ark/jsonschema/json.ts
@@ -0,0 +1,91 @@
+import type { JsonSchemaOrBoolean } from "@ark/schema"
+import { printable, throwParseError } from "@ark/util"
+import { type, type JsonSchema } from "arktype"
+import { parseJsonSchemaAnyKeywords } from "./any.ts"
+import { validateJsonSchemaArray } from "./array.ts"
+import { parseJsonSchemaCompositionKeywords } from "./composition.ts"
+import { validateJsonSchemaNumber } from "./number.ts"
+import { validateJsonSchemaObject } from "./object.ts"
+import { JsonSchemaScope } from "./scope.ts"
+import { validateJsonSchemaString } from "./string.ts"
+
+export const innerParseJsonSchema = JsonSchemaScope.Schema.pipe(
+	(jsonSchema: JsonSchemaOrBoolean): type.Any => {
+		if (typeof jsonSchema === "boolean") {
+			if (jsonSchema) return JsonSchemaScope.Json
+			else return type("never") // No runtime value ever passes validation for JSON schema of 'false'
+		}
+
+		if (Array.isArray(jsonSchema)) {
+			return (
+				parseJsonSchemaCompositionKeywords({ anyOf: jsonSchema }) ??
+				throwParseError(
+					"Failed to convert root array of JSON Schemas to an anyOf schema"
+				)
+			)
+		}
+
+		const constAndOrEnumValidator = parseJsonSchemaAnyKeywords(
+			jsonSchema as JsonSchema
+		)
+		const compositionValidator = parseJsonSchemaCompositionKeywords(
+			jsonSchema as JsonSchema
+		)
+
+		const preTypeValidator: type.Any | undefined =
+			constAndOrEnumValidator ?
+				compositionValidator ? compositionValidator.and(constAndOrEnumValidator)
+				:	constAndOrEnumValidator
+			:	compositionValidator
+
+		if ("type" in jsonSchema) {
+			let typeValidator: type.Any
+
+			if (Array.isArray(jsonSchema.type)) {
+				typeValidator =
+					parseJsonSchemaCompositionKeywords({
+						anyOf: jsonSchema.type.map(t => ({ type: t }))
+					}) ??
+					throwParseError(
+						"Failed to convert array of JSON Schemas types to an anyOf schema"
+					)
+			} else {
+				const jsonSchemaType = jsonSchema.type as JsonSchema.TypeName
+				switch (jsonSchemaType) {
+					case "array":
+						typeValidator = validateJsonSchemaArray.assert(jsonSchema)
+						break
+					case "boolean":
+					case "null":
+						typeValidator = type(jsonSchemaType)
+						break
+					case "integer":
+					case "number":
+						typeValidator = validateJsonSchemaNumber.assert(jsonSchema)
+						break
+					case "object":
+						typeValidator = validateJsonSchemaObject.assert(jsonSchema)
+						break
+					case "string":
+						typeValidator = validateJsonSchemaString.assert(jsonSchema)
+						break
+					default:
+						throwParseError(
+							`Provided 'type' value must be a supported JSON Schema type (was '${jsonSchemaType}')`
+						)
+				}
+			}
+			if (preTypeValidator === undefined) return typeValidator
+			return typeValidator.and(preTypeValidator)
+		}
+		if (preTypeValidator === undefined) {
+			throwParseError(
+				`Provided JSON Schema must have one of 'type', 'enum', 'const', 'allOf', 'anyOf' but was ${printable(jsonSchema)}.`
+			)
+		}
+		return preTypeValidator
+	}
+)
+
+export const parseJsonSchema = (jsonSchema: JsonSchemaOrBoolean): type.Any =>
+	innerParseJsonSchema.assert(jsonSchema) as never
diff --git a/ark/jsonschema/number.ts b/ark/jsonschema/number.ts
new file mode 100644
index 0000000000..623dd11cb4
--- /dev/null
+++ b/ark/jsonschema/number.ts
@@ -0,0 +1,47 @@
+import { rootSchema, type Intersection } from "@ark/schema"
+import { throwParseError } from "@ark/util"
+import type { JsonSchema, Out, Type } from "arktype"
+import { JsonSchemaScope } from "./scope.ts"
+
+export const validateJsonSchemaNumber: Type<
+	(In: JsonSchema.Numeric) => Out<Type<number, any>>,
+	any
+> = JsonSchemaScope.NumberSchema.pipe((jsonSchema): Type<number> => {
+	const arktypeNumberSchema: Intersection.Schema<number> = {
+		domain: "number"
+	}
+
+	if ("maximum" in jsonSchema) {
+		if ("exclusiveMaximum" in jsonSchema) {
+			throwParseError(
+				"Provided number JSON Schema cannot have 'maximum' and 'exclusiveMaximum"
+			)
+		}
+		arktypeNumberSchema.max = jsonSchema.maximum
+	} else if ("exclusiveMaximum" in jsonSchema) {
+		arktypeNumberSchema.max = {
+			rule: jsonSchema.exclusiveMaximum,
+			exclusive: true
+		}
+	}
+
+	if ("minimum" in jsonSchema) {
+		if ("exclusiveMinimum" in jsonSchema) {
+			throwParseError(
+				"Provided number JSON Schema cannot have 'minimum' and 'exclusiveMinimum"
+			)
+		}
+		arktypeNumberSchema.min = jsonSchema.minimum
+	} else if ("exclusiveMinimum" in jsonSchema) {
+		arktypeNumberSchema.min = {
+			rule: jsonSchema.exclusiveMinimum,
+			exclusive: true
+		}
+	}
+
+	if ("multipleOf" in jsonSchema)
+		arktypeNumberSchema.divisor = jsonSchema.multipleOf
+	else if (jsonSchema.type === "integer") arktypeNumberSchema.divisor = 1
+
+	return rootSchema(arktypeNumberSchema) as never
+})
diff --git a/ark/jsonschema/object.ts b/ark/jsonschema/object.ts
new file mode 100644
index 0000000000..9ea50bbabe
--- /dev/null
+++ b/ark/jsonschema/object.ts
@@ -0,0 +1,266 @@
+import {
+	ArkErrors,
+	rootSchema,
+	type Intersection,
+	type Predicate,
+	type TraversalContext
+} from "@ark/schema"
+import { conflatenateAll, getDuplicatesOf, printable } from "@ark/util"
+import type { JsonSchema, Out, Type } from "arktype"
+
+import { parseJsonSchema } from "./json.ts"
+import { JsonSchemaScope } from "./scope.ts"
+
+const parseMinMaxProperties = (
+	jsonSchema: JsonSchema.Object,
+	ctx: TraversalContext
+) => {
+	const predicates: Predicate.Schema[] = []
+	if ("maxProperties" in jsonSchema) {
+		const maxProperties = jsonSchema.maxProperties
+
+		if ((jsonSchema.required?.length ?? 0) > maxProperties) {
+			ctx.reject({
+				expected: `an object JSON Schema with at most ${jsonSchema.maxProperties} required properties`,
+				actual: `an object JSON Schema with ${jsonSchema.required!.length} required properties`
+			})
+		}
+		predicates.push((data: object, ctx) => {
+			const keys = Object.keys(data)
+			return keys.length <= maxProperties ?
+					true
+				:	ctx.reject({
+						expected: `an object with at most ${maxProperties} propert${maxProperties === 1 ? "y" : "ies"}`,
+						actual: `an object with ${keys.length.toString()} propert${maxProperties === 1 ? "y" : "ies"}`
+					})
+		})
+	}
+	if ("minProperties" in jsonSchema) {
+		const minProperties = jsonSchema.minProperties
+		predicates.push((data: object, ctx) => {
+			const keys = Object.keys(data)
+			return keys.length >= minProperties ?
+					true
+				:	ctx.reject({
+						expected: `an object with at least ${minProperties} propert${minProperties === 1 ? "y" : "ies"}`,
+						actual: `an object with ${keys.length.toString()} propert${minProperties === 1 ? "y" : "ies"}`
+					})
+		})
+	}
+	return predicates
+}
+
+const parsePatternProperties = (
+	jsonSchema: JsonSchema.Object,
+	ctx: TraversalContext
+) => {
+	if (!("patternProperties" in jsonSchema)) return
+
+	const patternProperties = Object.entries(jsonSchema.patternProperties).map(
+		([key, value]) => [new RegExp(key), parseJsonSchema(value)] as const
+	)
+
+	// Ensure that the schema for any property is compatible with any corresponding patternProperties
+	patternProperties.forEach(([pattern, parsedPatternPropertySchema]) => {
+		Object.entries(jsonSchema.properties ?? {}).forEach(
+			([property, schemaForProperty]) => {
+				if (!pattern.test(property)) return
+
+				const parsedPropertySchema = parseJsonSchema(schemaForProperty)
+
+				if (!parsedPropertySchema.overlaps(parsedPatternPropertySchema)) {
+					ctx.reject({
+						path: [property],
+						expected: `a JSON Schema that overlaps with the schema for patternProperty ${pattern} (${parsedPatternPropertySchema.description})`,
+						actual: parsedPropertySchema.description
+					})
+				}
+			}
+		)
+	})
+
+	// NB: We don't validate compatability of schemas for overlapping patternProperties
+	// since getting the intersection of regexes is inherently non-trivial.
+	return (data: object, ctx: TraversalContext) => {
+		Object.entries(data).forEach(([dataKey, dataValue]) => {
+			patternProperties.forEach(([pattern, parsedJsonSchema]) => {
+				if (pattern.test(dataKey) && !parsedJsonSchema.allows(dataValue)) {
+					ctx.reject({
+						path: [dataKey],
+						expected: `${parsedJsonSchema.description}, as property ${dataKey} matches patternProperty ${pattern}`,
+						actual: printable(dataValue)
+					})
+				}
+			})
+		})
+		return ctx.hasError()
+	}
+}
+
+const parsePropertyNames = (
+	jsonSchema: JsonSchema.Object,
+	ctx: TraversalContext
+) => {
+	if (!("propertyNames" in jsonSchema)) return
+
+	const propertyNamesValidator = parseJsonSchema(jsonSchema.propertyNames)
+
+	if (
+		"domain" in propertyNamesValidator.json &&
+		propertyNamesValidator.json.domain !== "string"
+	) {
+		ctx.reject({
+			path: ["propertyNames"],
+			expected: "a schema for validating a string",
+			actual: `a schema for validating a ${printable(propertyNamesValidator.json.domain)}`
+		})
+	}
+
+	return (data: object, ctx: TraversalContext) => {
+		Object.keys(data).forEach(key => {
+			if (!propertyNamesValidator.allows(key)) {
+				ctx.reject({
+					path: [key],
+					expected: `a key adhering to the propertyNames schema of ${propertyNamesValidator.description}`,
+					actual: key
+				})
+			}
+		})
+		return ctx.hasError()
+	}
+}
+
+const parseRequiredAndOptionalKeys = (
+	jsonSchema: JsonSchema.Object,
+	ctx: TraversalContext
+) => {
+	const optionalKeys: string[] = []
+	const requiredKeys: string[] = []
+	if ("properties" in jsonSchema) {
+		if ("required" in jsonSchema) {
+			const duplicateRequiredKeys = getDuplicatesOf(jsonSchema.required)
+			if (duplicateRequiredKeys.length !== 0) {
+				ctx.reject({
+					path: ["required"],
+					expected: "an array of unique strings",
+					actual: `an array with the following duplicates: ${printable(duplicateRequiredKeys)}`
+				})
+			}
+
+			for (const key of jsonSchema.required) {
+				if (key in jsonSchema.properties) requiredKeys.push(key)
+				else {
+					ctx.reject({
+						path: ["required"],
+						expected: `a key from the 'properties' object (one of ${printable(Object.keys(jsonSchema.properties))})`,
+						actual: key
+					})
+				}
+			}
+			for (const key in jsonSchema.properties)
+				if (!jsonSchema.required.includes(key)) optionalKeys.push(key)
+		} else {
+			// If 'required' is not present, all keys are optional
+			optionalKeys.push(...Object.keys(jsonSchema.properties))
+		}
+	} else if ("required" in jsonSchema) {
+		ctx.reject({
+			expected: "a valid object JSON Schema",
+			actual:
+				"an object JSON Schema with 'required' array but no 'properties' object"
+		})
+	}
+
+	return {
+		optionalKeys: optionalKeys.map(key => ({
+			key,
+			value: parseJsonSchema(jsonSchema.properties![key]).internal
+		})),
+		requiredKeys: requiredKeys.map(key => ({
+			key,
+			value: parseJsonSchema(jsonSchema.properties![key]).internal
+		}))
+	}
+}
+
+const parseAdditionalProperties = (jsonSchema: JsonSchema.Object) => {
+	if (!("additionalProperties" in jsonSchema)) return
+
+	const properties =
+		jsonSchema.properties ? Object.keys(jsonSchema.properties) : []
+	const patternProperties = Object.keys(jsonSchema.patternProperties ?? {})
+
+	const additionalPropertiesSchema = jsonSchema.additionalProperties
+	if (additionalPropertiesSchema === true) return
+
+	return (data: object, ctx: TraversalContext) => {
+		Object.keys(data).forEach(key => {
+			if (
+				properties.includes(key) ||
+				patternProperties.find(pattern => new RegExp(pattern).test(key))
+			)
+				// Not an additional property, so don't validate here
+				return
+
+			if (additionalPropertiesSchema === false) {
+				ctx.reject({
+					expected:
+						"an object with no additional keys, since provided additionalProperties JSON Schema doesn't allow it",
+					actual: `an additional key (${key})`
+				})
+				return
+			}
+
+			const additionalPropertyValidator = parseJsonSchema(
+				additionalPropertiesSchema
+			)
+
+			const value = data[key as keyof typeof data]
+			if (!additionalPropertyValidator.allows(value)) {
+				ctx.reject({
+					path: [key],
+					expected: `${additionalPropertyValidator.description}, since ${key} is an additional property.`,
+					actual: printable(value)
+				})
+			}
+		})
+		return ctx.hasError()
+	}
+}
+
+export const validateJsonSchemaObject: Type<
+	(In: JsonSchema.Object) => Out<Type<object, any>>,
+	any
+> = JsonSchemaScope.ObjectSchema.pipe((jsonSchema, ctx): Type<object> => {
+	const arktypeObjectSchema: Intersection.Schema<object> = {
+		domain: "object"
+	}
+
+	const { requiredKeys, optionalKeys } = parseRequiredAndOptionalKeys(
+		jsonSchema,
+		ctx
+	)
+	arktypeObjectSchema.required = requiredKeys
+	arktypeObjectSchema.optional = optionalKeys
+
+	const predicates = conflatenateAll<Predicate.Schema>(
+		...parseMinMaxProperties(jsonSchema, ctx),
+		parsePropertyNames(jsonSchema, ctx),
+		parsePatternProperties(jsonSchema, ctx),
+		parseAdditionalProperties(jsonSchema)
+	)
+
+	const typeWithoutPredicates = rootSchema(arktypeObjectSchema)
+	if (predicates.length === 0) return typeWithoutPredicates as never
+
+	return rootSchema({ domain: "object", predicate: predicates }).narrow(
+		(obj: object, innerCtx) => {
+			const validationResult = typeWithoutPredicates(obj)
+			if (validationResult instanceof ArkErrors) {
+				innerCtx.errors.merge(validationResult)
+				return false
+			}
+			return true
+		}
+	) as never
+})
diff --git a/ark/jsonschema/package.json b/ark/jsonschema/package.json
new file mode 100644
index 0000000000..7ffc255029
--- /dev/null
+++ b/ark/jsonschema/package.json
@@ -0,0 +1,41 @@
+{
+	"name": "@ark/jsonschema",
+	"version": "1.0.0",
+	"license": "MIT",
+	"author": {
+		"name": "TizzySaurus",
+		"email": "tizzysaurus@gmail.com",
+		"url": "https://github.com/tizzysaurus"
+	},
+	"repository": {
+		"type": "git",
+		"url": "https://github.com/arktypeio/arktype.git"
+	},
+	"type": "module",
+	"main": "./out/index.js",
+	"types": "./out/index.d.ts",
+	"exports": {
+		".": {
+			"ark-ts": "./index.ts",
+			"default": "./out/index.js"
+		},
+		"./internal/*.ts": {
+			"ark-ts": "./*.ts",
+			"default": "./out/*.js"
+		}
+	},
+	"files": [
+		"out"
+	],
+	"scripts": {
+		"build": "ts ../repo/build.ts",
+		"bench": "ts ./__tests__/comparison.bench.ts",
+		"test": "ts ../repo/testPackage.ts",
+		"tnt": "ts ../repo/testPackage.ts --skipTypes"
+	},
+	"dependencies": {
+		"arktype": "workspace:*",
+		"@ark/schema": "workspace:*",
+		"@ark/util": "workspace:*"
+	}
+}
diff --git a/ark/jsonschema/scope.ts b/ark/jsonschema/scope.ts
new file mode 100644
index 0000000000..2567ba30a9
--- /dev/null
+++ b/ark/jsonschema/scope.ts
@@ -0,0 +1,105 @@
+import type { JsonSchemaOrBoolean } from "@ark/schema"
+import { type JsonSchema, scope, type Scope } from "arktype"
+
+type AnyKeywords = Partial<JsonSchema.Const & JsonSchema.Enum>
+
+type TypeWithNoKeywords = { type: "boolean" | "null" }
+type TypeWithKeywords =
+	| JsonSchema.Array
+	| JsonSchema.Numeric
+	| JsonSchema.Object
+	| StringSchema
+// NB: For sake of simplicitly, at runtime it's assumed that
+// whatever we're parsing is valid JSON since it will be 99% of the time.
+// This decision may be changed later, e.g. when a built-in JSON type exists in AT.
+type Json = unknown
+
+type ArraySchema = JsonSchema.Array
+
+type NumberSchema = JsonSchema.Numeric
+
+type ObjectSchema = JsonSchema.Object
+
+// NB: @ark/jsonschema doesn't support the "format" keyword, and the "pattern" could be string|RegExp rather than only string, so we need a separate type
+export type StringSchema = Omit<JsonSchema.String, "format" | "pattern"> & {
+	pattern?: string | RegExp
+}
+
+type JsonSchemaScope = Scope<{
+	AnyKeywords: AnyKeywords
+	CompositionKeywords: JsonSchema.Composition
+	TypeWithNoKeywords: TypeWithNoKeywords
+	TypeWithKeywords: TypeWithKeywords
+	Json: Json
+	Schema: JsonSchemaOrBoolean
+	ArraySchema: ArraySchema
+	NumberSchema: NumberSchema
+	ObjectSchema: ObjectSchema
+	StringSchema: StringSchema
+}>
+
+const $: JsonSchemaScope = scope({
+	AnyKeywords: {
+		"const?": "unknown",
+		"enum?": "unknown[]"
+	},
+	CompositionKeywords: {
+		"allOf?": "Schema[]",
+		"anyOf?": "Schema[]",
+		"oneOf?": "Schema[]",
+		"not?": "Schema"
+	},
+	TypeWithNoKeywords: { type: "'boolean'|'null'" },
+	TypeWithKeywords: "ArraySchema|NumberSchema|ObjectSchema|StringSchema",
+	// NB: For sake of simplicitly, at runtime it's assumed that
+	// whatever we're parsing is valid JSON since it will be 99% of the time.
+	// This decision may be changed later, e.g. when a built-in JSON type exists in AT.
+	Json: "unknown",
+	"#BaseSchema":
+		// NB: `true` means "accept an valid JSON"; `false` means "reject everything".
+		"boolean|TypeWithNoKeywords|TypeWithKeywords|AnyKeywords|CompositionKeywords",
+	Schema: "BaseSchema|BaseSchema[]",
+	ArraySchema: {
+		"additionalItems?": "Schema",
+		"contains?": "Schema",
+		// JSON Schema states that if 'items' is not present, then treat as an empty schema (i.e. accept any valid JSON)
+		"items?": "Schema|Schema[]",
+		"maxItems?": "number.integer>=0",
+		"minItems?": "number.integer>=0",
+		// NB: Technically `prefixItems` and `items` are mutually exclusive,
+		// which is reflected at runtime but it's not worth the performance cost to validate this statically.
+		"prefixItems?": "Schema[]",
+		type: "'array'",
+		"uniqueItems?": "boolean"
+	},
+	NumberSchema: {
+		// NB: Technically 'exclusiveMaximum' and 'exclusiveMinimum' are mutually exclusive with 'maximum' and 'minimum', respectively,
+		// which is reflected at runtime but it's not worth the performance cost to validate this statically.
+		"exclusiveMaximum?": "number",
+		"exclusiveMinimum?": "number",
+		"maximum?": "number",
+		"minimum?": "number",
+		// NB: JSON Schema allows decimal multipleOf, but ArkType only supports integer.
+		"multipleOf?": "number.integer",
+		type: "'number'|'integer'"
+	},
+	ObjectSchema: {
+		"additionalProperties?": "Schema",
+		"maxProperties?": "number.integer>=0",
+		"minProperties?": "number.integer>=0",
+		"patternProperties?": { "[string]": "Schema" },
+		// NB: Technically 'properties' is required when 'required' is present,
+		// which is reflected at runtime but it's not worth the performance cost to validate this statically.
+		"properties?": { "[string]": "Schema" },
+		"propertyNames?": "Schema",
+		"required?": "string[]",
+		type: "'object'"
+	},
+	StringSchema: {
+		"maxLength?": "number.integer>=0",
+		"minLength?": "number.integer>=0",
+		"pattern?": "RegExp | string",
+		type: "'string'"
+	}
+}) as unknown as JsonSchemaScope
+export const JsonSchemaScope = $.export()
diff --git a/ark/jsonschema/string.ts b/ark/jsonschema/string.ts
new file mode 100644
index 0000000000..8aa835f41c
--- /dev/null
+++ b/ark/jsonschema/string.ts
@@ -0,0 +1,26 @@
+import { rootSchema, type Intersection } from "@ark/schema"
+import type { Out, Type } from "arktype"
+import { JsonSchemaScope, type StringSchema } from "./scope.ts"
+
+export const validateJsonSchemaString: Type<
+	(In: StringSchema) => Out<Type<string, any>>,
+	any
+> = JsonSchemaScope.StringSchema.pipe((jsonSchema): Type<string> => {
+	const arktypeStringSchema: Intersection.Schema<string> = {
+		domain: "string"
+	}
+
+	if ("maxLength" in jsonSchema)
+		arktypeStringSchema.maxLength = jsonSchema.maxLength
+	if ("minLength" in jsonSchema)
+		arktypeStringSchema.minLength = jsonSchema.minLength
+	if ("pattern" in jsonSchema) {
+		if (jsonSchema.pattern instanceof RegExp) {
+			arktypeStringSchema.pattern = [
+				// Strip leading and trailing slashes from RegExp
+				jsonSchema.pattern.toString().slice(1, -1)
+			]
+		} else arktypeStringSchema.pattern = [jsonSchema.pattern]
+	}
+	return rootSchema(arktypeStringSchema) as never
+})
diff --git a/ark/jsonschema/tsconfig.build.json b/ark/jsonschema/tsconfig.build.json
new file mode 120000
index 0000000000..f74ef64d4c
--- /dev/null
+++ b/ark/jsonschema/tsconfig.build.json
@@ -0,0 +1 @@
+../repo/tsconfig.esm.json
\ No newline at end of file
diff --git a/ark/schema/shared/jsonSchema.ts b/ark/schema/shared/jsonSchema.ts
index 4709223423..b3eee3ed3d 100644
--- a/ark/schema/shared/jsonSchema.ts
+++ b/ark/schema/shared/jsonSchema.ts
@@ -1,13 +1,16 @@
 import {
 	printable,
 	throwInternalError,
+	type array,
 	type JsonArray,
 	type JsonObject,
 	type listable
 } from "@ark/util"
 import type { ConstraintKind } from "./implement.ts"
 
-export type JsonSchema = JsonSchema.Union | JsonSchema.Branch
+export type JsonSchema = JsonSchema.NonBooleanBranch
+export type ListableJsonSchema = listable<JsonSchema>
+export type JsonSchemaOrBoolean = listable<JsonSchema.Branch>
 
 export declare namespace JsonSchema {
 	export type TypeName =
@@ -31,30 +34,61 @@ export declare namespace JsonSchema {
 		examples?: readonly t[]
 	}
 
-	export type Branch = Constrainable | Const | String | Numeric | Object | Array
+	type Composition = Union | OneOf | Intersection | Not
+
+	type NonBooleanBranch =
+		| Constrainable
+		| Const
+		| Composition
+		| Enum
+		| String
+		| Numeric
+		| Object
+		| Array
+
+	export type Branch = boolean | JsonSchema
 
 	export interface Constrainable extends Meta {
 		type?: listable<TypeName>
 	}
 
+	export interface Intersection extends Meta {
+		allOf: readonly JsonSchema[]
+	}
+
+	export interface Not {
+		not: JsonSchema
+	}
+
+	export interface OneOf extends Meta {
+		oneOf: readonly JsonSchema[]
+	}
+
 	export interface Union extends Meta {
-		anyOf: readonly Branch[]
+		anyOf: readonly JsonSchema[]
 	}
 
 	export interface Const extends Meta {
 		const: unknown
 	}
 
+	export interface Enum extends Meta {
+		enum: array
+	}
+
 	export interface String extends Meta<string> {
 		type: "string"
 		minLength?: number
 		maxLength?: number
-		pattern?: string
+		pattern?: string | RegExp
 		format?: string
 	}
 
+	// NB: Technically 'exclusiveMaximum' and 'exclusiveMinimum' are mutually exclusive with 'maximum' and 'minimum', respectively,
+	// which is reflected at runtime but it's not worth the performance cost to validate this statically.
 	export interface Numeric extends Meta<number> {
 		type: "number" | "integer"
+		// NB: JSON Schema allows decimal multipleOf, but ArkType only supports integer.
 		multipleOf?: number
 		minimum?: number
 		exclusiveMinimum?: number
@@ -62,20 +96,28 @@ export declare namespace JsonSchema {
 		exclusiveMaximum?: number
 	}
 
+	// NB: Technically 'properties' is required when 'required' is present,
+	// which is reflected at runtime but it's not worth the performance cost to validate this statically.
 	export interface Object extends Meta<JsonObject> {
 		type: "object"
 		properties?: Record<string, JsonSchema>
 		required?: string[]
 		patternProperties?: Record<string, JsonSchema>
-		additionalProperties?: false | JsonSchema
+		additionalProperties?: JsonSchemaOrBoolean
+		maxProperties?: number
+		minProperties?: number
+		propertyNames?: String
 	}
 
 	export interface Array extends Meta<JsonArray> {
 		type: "array"
+		additionalItems?: JsonSchemaOrBoolean
+		contains?: JsonSchemaOrBoolean
+		uniqueItems?: boolean
 		minItems?: number
 		maxItems?: number
-		items?: JsonSchema | false
-		prefixItems?: readonly JsonSchema[]
+		items?: JsonSchemaOrBoolean
+		prefixItems?: readonly Branch[]
 	}
 
 	export type LengthBoundable = String | Array
diff --git a/ark/type/__tests__/imports.test.ts b/ark/type/__tests__/imports.test.ts
index fb049c06c7..87aacdc783 100644
--- a/ark/type/__tests__/imports.test.ts
+++ b/ark/type/__tests__/imports.test.ts
@@ -76,8 +76,8 @@ contextualize(() => {
 
 				// have to snapshot the module since TypeScript treats it as bivariant
 				attest(types).type.toString.snap(`Module<{
-	public: true | 3 | uuid | "no"
 	hasCrept: true
+	public: true | 3 | uuid | "no"
 }>`)
 			})
 		}
diff --git a/ark/type/index.ts b/ark/type/index.ts
index 5a170f90ab..9f30614b17 100644
--- a/ark/type/index.ts
+++ b/ark/type/index.ts
@@ -6,7 +6,7 @@ export {
 	type JsonSchema
 } from "@ark/schema"
 export { Hkt, inferred } from "@ark/util"
-export type { distill } from "./attributes.ts"
+export type { distill, number, Out, string } from "./attributes.ts"
 export * from "./config.ts"
 export { Generic } from "./generic.ts"
 export {
@@ -21,3 +21,4 @@ export {
 export { Module, type BoundModule, type Submodule } from "./module.ts"
 export { module, scope, type Scope } from "./scope.ts"
 export { Type } from "./type.ts"
+
diff --git a/ark/util/arrays.ts b/ark/util/arrays.ts
index e14c6bb8fb..c6785753b9 100644
--- a/ark/util/arrays.ts
+++ b/ark/util/arrays.ts
@@ -3,6 +3,34 @@ import type { anyOrNever, conform } from "./generics.ts"
 import type { isDisjoint } from "./intersections.ts"
 import type { parseNonNegativeInteger } from "./numbers.ts"
 
+type DuplicateData<val = unknown> = { element: val; indices: number[] }
+
+/**
+ * Extracts duplicated elements and their indices from an array, returning them.
+ *
+ * @param arr The array to extract duplicate elements from.
+ */ export const getDuplicatesOf = <const arr extends array>(
+	arr: arr,
+	opts?: ComparisonOptions<arr[number]>
+): DuplicateData<arr[number]>[] => {
+	const isEqual = opts?.isEqual ?? ((l, r) => l === r)
+
+	const seenElements: Set<arr[number]> = new Set()
+	const duplicates: DuplicateData<arr[number]>[] = []
+
+	arr.forEach((element, indx) => {
+		if (seenElements.has(element)) {
+			const duplicatesEntryIndx = duplicates.findIndex((l, r) =>
+				isEqual(l.element, r)
+			)
+			if (duplicatesEntryIndx === -1)
+				duplicates.push({ element, indices: [indx] })
+			else duplicates[duplicatesEntryIndx].indices.push(indx)
+		} else seenElements.add(element)
+	})
+	return duplicates
+}
+
 export type pathToString<
 	segments extends string[],
 	delimiter extends string = "/"
diff --git a/ark/util/package.json b/ark/util/package.json
index 550e0ed022..fec368e0a3 100644
--- a/ark/util/package.json
+++ b/ark/util/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@ark/util",
-	"version": "0.25.0",
+	"version": "0.26.0",
 	"license": "MIT",
 	"author": {
 		"name": "David Blass",
diff --git a/ark/util/registry.ts b/ark/util/registry.ts
index edeb954b62..9e203df908 100644
--- a/ark/util/registry.ts
+++ b/ark/util/registry.ts
@@ -7,7 +7,7 @@ import { FileConstructor, objectKindOf } from "./objectKinds.ts"
 // recent node versions (https://nodejs.org/api/esm.html#json-modules).
 
 // For now, we assert this matches the package.json version via a unit test.
-export const arkUtilVersion = "0.25.0"
+export const arkUtilVersion = "0.26.0"
 
 export const initialRegistryContents = {
 	version: arkUtilVersion,
diff --git a/package.json b/package.json
index 06a8293f60..ffdd0f27c2 100644
--- a/package.json
+++ b/package.json
@@ -49,6 +49,7 @@
 		"@ark/attest-ts-min": "catalog:",
 		"@ark/attest-ts-next": "catalog:",
 		"@ark/fs": "workspace:*",
+		"@ark/jsonschema": "workspace:*",
 		"@ark/repo": "workspace:*",
 		"@ark/util": "workspace:*",
 		"@eslint/js": "9.14.0",