jsonchema.inferFromObject syscall

This commit is contained in:
Zef Hemel
2026-06-30 21:53:19 +02:00
parent 283022c3e7
commit 6f8c2ed55a
4 changed files with 114 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
import { expect, test } from "vitest";
import { inferFromObject } from "./jsonschema.ts";
test("inferFromObject maps primitive field types", () => {
const schema = inferFromObject({
name: "Foo",
count: 3,
ratio: 1.5,
done: false,
missing: null,
});
expect(schema.type).toBe("object");
expect(schema["x-inferred"]).toBe(true);
expect(schema.properties.name).toEqual({ type: "string" });
expect(schema.properties.count).toEqual({ type: "integer" });
expect(schema.properties.ratio).toEqual({ type: "number" });
expect(schema.properties.done).toEqual({ type: "boolean" });
expect(schema.properties.missing).toEqual({ type: "null" });
});
test("inferFromObject recurses into arrays and nested objects", () => {
const schema = inferFromObject({
tags: ["a", "b"],
meta: { owner: "z", weight: 2 },
empty: [],
});
expect(schema.properties.tags).toEqual({
type: "array",
items: { type: "string" },
});
expect(schema.properties.meta).toEqual({
type: "object",
properties: { owner: { type: "string" }, weight: { type: "integer" } },
});
// An empty array has no element to learn `items` from.
expect(schema.properties.empty).toEqual({ type: "array" });
});
test("inferFromObject handles a non-object top-level value", () => {
const schema = inferFromObject("hello");
expect(schema.type).toBe("string");
expect(schema["x-inferred"]).toBe(true);
});
+50
View File
@@ -75,6 +75,53 @@ export function validateSchema(schema: any): undefined | string {
return; return;
} }
/**
* Best-effort: infer a JSON Schema (draft 2020-12) from the *shape* of a single
* sample value. Types are guessed from one example, so the result is a hint,
* not a contract — the top-level schema is marked `"x-inferred": true`.
*
* Useful when a tag/object type has no declared schema but an example object
* exists and you want a plausible schema for it.
*/
export function inferFromObject(value: any): any {
const schema = inferSchemaNode(value);
schema["$schema"] = "https://json-schema.org/draft/2020-12/schema";
schema["x-inferred"] = true;
return schema;
}
/** Infer a bare JSON Schema node for a value, recursing into arrays/objects. */
function inferSchemaNode(value: any): any {
if (value === null || value === undefined) {
return { type: "null" };
}
if (Array.isArray(value)) {
const node: any = { type: "array" };
if (value.length > 0) {
node.items = inferSchemaNode(value[0]);
}
return node;
}
switch (typeof value) {
case "boolean":
return { type: "boolean" };
case "number":
return { type: Number.isInteger(value) ? "integer" : "number" };
case "string":
return { type: "string" };
case "object": {
const properties: Record<string, any> = {};
for (const [k, v] of Object.entries(value)) {
properties[k] = inferSchemaNode(v);
}
return { type: "object", properties };
}
default:
// functions, symbols, bigint, … — not representable in JSON Schema.
return {};
}
}
export function jsonschemaSyscalls(): SysCallMapping { export function jsonschemaSyscalls(): SysCallMapping {
return { return {
"jsonschema.validateObject": ( "jsonschema.validateObject": (
@@ -87,5 +134,8 @@ export function jsonschemaSyscalls(): SysCallMapping {
"jsonschema.validateSchema": (_ctx, schema: any): undefined | string => { "jsonschema.validateSchema": (_ctx, schema: any): undefined | string => {
return validateSchema(schema); return validateSchema(schema);
}, },
"jsonschema.inferFromObject": (_ctx, object: any): any => {
return inferFromObject(object);
},
}; };
} }
+12
View File
@@ -32,6 +32,18 @@ else
end end
``` ```
### jsonschema.inferFromObject(object)
Infers a best-effort JSON schema from the *shape* of a single sample value. Types are guessed from one example, so the result is a hint rather than a contract — the returned schema is marked with `"x-inferred": true`. Useful when an object type has no declared schema but you have an example to learn from.
Example:
```lua
local sample = { name = "Widget", count = 3, tags = { "a", "b" } }
local schema = jsonschema.inferFromObject(sample)
-- schema.properties.name.type == "string"
-- schema.properties.count.type == "integer"
-- schema.properties.tags.type == "array" (items.type == "string")
```
### jsonschema.validateSchema(schema) ### jsonschema.validateSchema(schema)
Validates a JSON schema itself to ensure it's well-formed. Validates a JSON schema itself to ensure it's well-formed.
+9
View File
@@ -21,3 +21,12 @@ export function validateObject(
export function validateSchema(schema: any): Promise<string | undefined> { export function validateSchema(schema: any): Promise<string | undefined> {
return syscall("jsonschema.validateSchema", schema); return syscall("jsonschema.validateSchema", schema);
} }
/**
* Infers a best-effort JSON schema from a sample value's shape.
* @param object the sample value to infer a schema from
* @returns an inferred JSON schema (draft 2020-12), marked `x-inferred`
*/
export function inferFromObject(object: any): Promise<any> {
return syscall("jsonschema.inferFromObject", object);
}