Skip to content
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,13 @@ These are available from the `@hyperjump/json-schema/experimental` export.

A curried function for validating an instance against a compiled schema.
This can be useful for creating custom output formats.
* **serialize**: (compiledSchema: CompiledSchema) => string

Serialize a compiled schema as JSON so it can be restored at a later time
without needing to recompile.
* **deserialize**: (serialized: string) => CompiledSchema

Restore a serialized compiled schema.

* **OutputFormat**: **FLAG** | **BASIC**

Expand Down
58 changes: 58 additions & 0 deletions lib/compiled-schema-serialization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as Pact from "@hyperjump/pact";
import { getKeyword } from "./keywords.js";


const REGEXP_MARKER = "a6d8f3e1-9b2c-4f7a-8d5b-1c2e3f4a5b6c";

export const serialize = (compiledSchema) => {
const plugins = [];
for (const plugin of compiledSchema.ast.plugins) {
if (!plugin.id) {
throw Error("Cannot serialize plugin without id");
}
plugins.push(plugin.id);
}

const toSerialize = {
...compiledSchema,
ast: {
...compiledSchema.ast,
plugins
}
};

return JSON.stringify(toSerialize, (_key, value) => {
if (value instanceof RegExp) {
return { [REGEXP_MARKER]: { source: value.source, flags: value.flags } };
}

return value;
});
};

export const deserialize = (serialized) => {
const parsed = JSON.parse(serialized, (_key, value) => {
if (value?.[REGEXP_MARKER]) {
return new RegExp(value[REGEXP_MARKER].source, value[REGEXP_MARKER].flags);
}

return value;
});

parsed.ast.plugins = Pact.pipe(
parsed.ast.plugins,
Pact.map((pluginUri) => resolvePlugin(pluginUri)),
Pact.collectSet
);

return parsed;
};

const resolvePlugin = (pluginUri) => {
const keyword = getKeyword(pluginUri);
if (keyword?.plugin?.id === pluginUri) {
return keyword.plugin;
}

throw Error(`Plugin with id '${pluginUri}' is not found`);
};
51 changes: 51 additions & 0 deletions lib/compiled-schema-serialization.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { afterEach, describe, expect, test } from "vitest";
import { registerSchema, unregisterSchema } from "../v1/index.js";
import { compile, deserialize, getSchema, interpret, serialize } from "./experimental.js";
import * as Instance from "./instance.js";


describe("Compiled Schema Serialization", () => {
const schemaUri = "schema:compiled-serialization";
const dialectUri = "https://json-schema.org/v1";

afterEach(() => {
unregisterSchema(schemaUri);
});

test("round-trips RegExp keyword values", async () => {
registerSchema({ pattern: "^a+$" }, schemaUri, dialectUri);
const schema = await getSchema(schemaUri);
const compiled = await compile(schema);

const restored = deserialize(serialize(compiled));

expect(interpret(restored, Instance.fromJs("aaa"))).to.eql({ valid: true });
expect(interpret(restored, Instance.fromJs("bbb"))).to.eql({ valid: false });
});

test("restores built-in evaluation plugins", async () => {
registerSchema({ unevaluatedProperties: false }, schemaUri, dialectUri);
const schema = await getSchema(schemaUri);
const compiled = await compile(schema);

const restored = deserialize(serialize(compiled));

expect(interpret(restored, Instance.fromJs({ extra: 1 }))).to.eql({ valid: false });
});

test("throws if plugin id cannot be resolved", () => {
const pluginUri = "https://example.com/plugins/missing";
const serialized = JSON.stringify({
schemaUri: "schema:missing#",
ast: {
"metaData": {},
"plugins": [pluginUri],
"schema:missing#": true
}
});

expect(() => {
deserialize(serialized);
}).to.throw(`Plugin with id '${pluginUri}' is not found`);
});
});
5 changes: 4 additions & 1 deletion lib/experimental.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ export type CompiledSchema = {
ast: AST;
};

export const serialize: (compiledSchema: CompiledSchema) => string;
export const deserialize: (serialized: string) => CompiledSchema;

type AST = {
metaData: Record<string, MetaData>;
plugins: EvaluationPlugin[];
plugins: Set<EvaluationPlugin>;
} & Record<string, Node<unknown>[] | boolean>;

type Node<A> = [keywordId: string, schemaUri: string, keywordValue: A];
Expand Down
1 change: 1 addition & 0 deletions lib/experimental.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export { default as Validation } from "./keywords/validation.js";
export * from "./evaluation-plugins/basic-output.js";
export * from "./evaluation-plugins/detailed-output.js";
export * from "./evaluation-plugins/annotations.js";
export * from "./compiled-schema-serialization.js";
Loading