JSON Schema Generator: Enhance Domain Validation & CI
Hey guys! Today, we're diving deep into a crucial task: closing the loop between our internal Zod domain schema and the public JSON Schema exposed at https://awesome-pages.github.io/schemas/domain/v1.json. Currently, we generate a JSON Schema using generateDomainV1JsonSchema, but it's not fully integrated. The parser's emitted domain artifact doesn't include the $schema property, and our CI doesn't validate domain.json outputs against the generated schema. This article will guide you through making JSON Schema generation a core part of our project, ensuring every domain artifact includes $schema, and adding an Ajv test/CI check to prevent discrepancies between the implementation and the published schema.
Acceptance Criteria
To ensure we're on the right track, here’s what we need to accomplish:
- Single, Canonical JSON Schema: We need one authoritative JSON Schema for the domain format, generated from
DomainV1Baseand located atschemas/domain/v1.jsonin the repository. This schema should include:"$id": "https://awesome-pages.github.io/schemas/domain/v1.json""$ref": "#/definitions/awesome-pages-domain.v1"(or an equivalent top-level reference)
$schemain Domain Artifacts: Everydomainartifact produced by the parser must include a$schemafield at the root. For example:"$schema": "https://awesome-pages.github.io/schemas/domain/v1.json"
- Ajv Validation Test: We need a test (or test suite) that validates at least one real
domain.jsonfixture against the generated schema using Ajv.- The test should fail if:
- The schema and implementation diverge.
- The fixture is no longer valid for the current Domain V1 shape.
- The test should fail if:
- CI Integration: The CI pipeline must include this validation step and fail if there’s a mismatch.
Implementation Notes
Let's break down how we’ll achieve these goals.
1. Expose and Wire generateDomainV1JsonSchema
First, we need to ensure we have a function that generates the JSON Schema. This involves creating a function to expose and wire generateDomainV1JsonSchema. We will leverage zod-to-json-schema to convert our Zod schema into a JSON Schema format. It's crucial to have a reliable, automated way to produce this schema. This function serves as the cornerstone for ensuring our schema accurately reflects our domain model.
import { zodToJsonSchema } from 'zod-to-json-schema';
import { DomainV1Base } from '@/schemas/domain.v1';
export function generateDomainV1JsonSchema() {
return zodToJsonSchema(DomainV1Base, {
name: 'awesome-pages-domain.v1',
$refStrategy: 'none',
});
}
Next, we create a small script (e.g., scripts/generate-schema.ts) to call this function, inject the $id, and write the output to schemas/domain/v1.json. This script is essential for automating the schema generation process, eliminating manual updates and reducing the risk of human error. By automating this, we ensure that the schema is always up-to-date and consistent with the domain model.
// scripts/generate-schema.ts
import { generateDomainV1JsonSchema } from '../src/schemas/domain.v1';
import * as fs from 'fs';
import * as path from 'path';
const schema = generateDomainV1JsonSchema();
schema['$id'] = 'https://awesome-pages.github.io/schemas/domain/v1.json';
const schemaPath = path.resolve(__dirname, '../schemas/domain/v1.json');
fs.writeFileSync(schemaPath, JSON.stringify(schema, null, 2));
console.log(`JSON Schema written to ${schemaPath}`);
Finally, we add a package.json script to execute this generation process. This ensures that generating the schema is as simple as running a command. The ease of use encourages developers to keep the schema updated.
"generate:schema": "tsx scripts/generate-schema.ts"
2. Inject $schema into Emitted domain JSON
To ensure that every domain artifact includes a reference to the correct schema, we need to inject the $schema property during the serialization process. This step is critical for enabling consumers of our domain artifacts to validate them against the schema. Without this, validation becomes manual and error-prone.
In the place where the domain artifact is serialized (e.g., emitArtifact("domain", ...) or equivalent), we wrap the domain object with $schema before JSON.stringify. This ensures that the $schema property is always present at the root of the JSON object. The wrapping ensures consistency across all emitted domain artifacts.
const withSchema = {
$schema: 'https://awesome-pages.github.io/schemas/domain/v1.json',
...domain,
};
// Then, serialize withSchema instead of domain
While we can optionally allow this to be configurable in the future, for now, we'll always emit $schema. This approach simplifies the initial implementation and ensures that validation is always possible.
3. Ajv Validation Test
To guarantee that our implementation adheres to the JSON Schema, we need to add an Ajv validation test. This test is vital for catching any discrepancies between the generated schema and the actual domain objects. It serves as a safety net, preventing invalid data from propagating through our system.
We add a new test file, for example, src/schemas/domain.v1.schema.spec.ts (or under tests/). This dedicated test file keeps our schema validation separate from other tests, making it easier to maintain and debug.
In the test, we import Ajv (draft-07) and ajv-formats for date-time. We load schemas/domain/v1.json and a real domain fixture (e.g., from src/tests/fixtures/expected/*.domain.json). Using a real-world fixture ensures that our test covers realistic scenarios and potential edge cases.
We then validate the fixture against the schema and assert no errors. This assertion is the core of our test, ensuring that the fixture conforms to the schema.
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import schema from '../../schemas/domain/v1.json';
import exampleDomain from '../fixtures/expected/awesome-click-and-use.domain.json';
const ajv = new Ajv({ allErrors: true, strict: true });
addFormats(ajv);
const validate = ajv.compile(schema);
it('validates domain fixture against JSON Schema', () => {
const ok = validate(exampleDomain);
if (!ok) {
console.error(validate.errors);
}
expect(ok).toBe(true);
});
We wire a script to run this test: `