VSON — Validation Schema Objects in Notation A DSL that compiles to JSON Schema Draft 2020-12
Getting started: Run pnpm app to launch the validation UI and backend. From the UI you can compile VSON, validate JSON against generated schemas, and run the full workflow. For CLI-only use, run pnpm compile:default after building.
- Node.js v20 or higher (download)
- pnpm v10 or higher
To install pnpm:
npm install -g pnpmpnpm install # Install root dependencies
cd ui
pnpm install # Install UI dependencies
cd ..
pnpm app # Open validation UI and start backend (compile from the UI)
pnpm test # Run tests
pnpm compile:default # Compile input/user.vson → output/schema.json (CLI)src/
├── index.ts # Entry point, CLI runner
├── cli.ts # CLI argument parsing and help text
├── compiler.ts # compile() and tryCompile() functions
├── server.ts # HTTP backend for the validation UI
│
├── grammars/ # ANTLR grammar definitions
│ ├── VSONLexer.g4 # Token definitions
│ └── VSONParser.g4 # Grammar rules
│
├── lexer/
│ ├── lexer.ts # tokenize(source) → Token[]
│ └── VSONLexer.ts # ANTLR-generated TypeScript (do not edit)
│
├── parser/
│ ├── parser.ts # parse(), parseToCST(), Parser class
│ ├── util.ts # Parser utilities (error context paths)
│ ├── VSONParser.ts # ANTLR-generated TypeScript (do not edit)
│ ├── VSONParserListener.ts # ANTLR-generated listener (do not edit)
│ └── VSONParserVisitor.ts # ANTLR-generated visitor (do not edit)
│
├── ast/
│ ├── ast.ts # AST node interfaces + type guards
│ └── visitor.ts # Visitor pattern base class
│
├── errors/
│ ├── context.ts # ErrorContext — structured error collection
│ └── listener.ts # ANTLR ErrorListener with line/column info
│
├── evaluator/
│ ├── evaluator.ts # Evaluator class, EvaluationContext
│ ├── evaluation-visitor.ts # Visitor that builds context from AST
│ ├── type-check-visitor.ts # Example type-check visitor (optional)
│ └── generator.ts # Converts context → JSON Schema
│
└── builtin/
└── builtin.ts # Built-in type → JSON Schema mappings
ui/ # React validation UI (Vite + TypeScript)
tests/
├── lexer.test.ts # Token output tests
├── parser.test.ts # CST structure tests
├── ast.test.ts # AST / parse() error handling and shape
└── basic.test.ts # End-to-end compile() tests
The compiler turns .vson source into JSON Schema in four stages:
- Lexing — character stream → tokens (VSONLexer)
- Parsing — tokens → CST → AST (VSONParser,
buildAST()). CustomErrorListenerreports syntax errors with line/column info. - Evaluation — AST → evaluation context (EvaluationVisitor: schema declarations, field declarations, mutate statements, conditionals with
has()/&&/||/!). Field types are resolved viaresolveType(): built-in types, arrays (Type[]), and schema composition (references to other schemas → inline object schema). - Generation — context → JSON Schema Draft 2020-12 (Generator); multiple schemas emitted when composition is used.
Diagram: pipeline.mmd (Mermaid). Rendered: 
schema User {
name: string;
age: integer;
toBeRemoved: boolean @optional;
}
if (has(User, "name") || has(User, "missingField")) {
add(User, "requiredField", boolean);
if (!has(User, "missingField")) {
addOptional(User, "missingField", integer);
}
addOptional(User, "optionalField", string);
remove(User, "toBeRemoved");
}
Built-in types: string, integer, float, number, boolean → JSON Schema types. Any built-in type can be used as an
array with Type[] (e.g. string[], integer[]). Fields can be marked inline optional with @optional.
Schema composition: a field type can be another schema name (e.g. address: Address, waitlist: User[]), producing nested or array-of-object output; schemas must be declared before use and cannot be circular. Mutable statements (add, addOptional, remove) modify the schema after declaration.
Conditionals (if) use has(Schema, "field") with logical operators (&&, ||, !) and support nesting.
You can reference other schemas as field types. Each schema is emitted as a separate JSON Schema object; composition is inlined (no $ref). Declaration order matters: a schema must be declared before it is used. Circular references are not allowed.
- Single schema:
fieldName: SchemaName;(e.g.instructor: User) - Array of schema:
fieldName: SchemaName[];(e.g.waitlist: User[])
schema Address {
city: string;
}
schema User {
name: string;
address: Address;
}
schema Course {
instructor: User;
waitlist: User[];
}
The compiler outputs one JSON Schema per declared schema (Address, User, Course). The instructor and address fields are inlined as type: "object" with the corresponding properties and required.
Mutable operations allow post-declaration schema modification. They execute sequentially after the schema declaration.
| Operation | Syntax | Description |
|---|---|---|
add |
add(SchemaName, "fieldName", type); |
Adds a required field to an existing schema |
addOptional |
addOptional(SchemaName, "fieldName", type); |
Adds an optional field to an existing schema |
remove |
remove(SchemaName, "fieldName"); |
Removes a field from an existing schema |
Types in mutable operations can be scalars (string, integer, …) or arrays (string[], float[], …).
Input:
schema User {
name: string;
}
add(User, "age", integer);
Output:
{
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name", "age"]
}Target: JSON Schema Draft 2020-12
Input (matches input/user.vson):
schema User {
name: string;
age: integer;
toBeRemoved: boolean @optional;
}
if (has(User, "name") || has(User, "missingField")) {
add(User, "requiredField", boolean);
if (!has(User, "missingField")) {
addOptional(User, "missingField", integer);
}
addOptional(User, "optionalField", string);
remove(User, "toBeRemoved");
}
Output (output/schema.json):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"},
"requiredField": {"type": "boolean"},
"missingField": {"type": "integer"},
"optionalField": {"type": "string"}
},
"required": ["name", "age", "requiredField"]
}| Command | Description |
|---|---|
pnpm install |
Install dependencies |
pnpm test |
Run Jest tests (pretest runs scripts/pretest.ts first) |
pnpm test tests/ast.test.ts |
Run specific test files, e.g. tests/ast.test.ts |
pnpm build |
Compile TypeScript to dist/ |
pnpm dev -- <args> |
Run CLI without building (e.g. pnpm dev -- compile ...) |
pnpm start -- <args> |
Run built CLI (node dist/index.js) |
pnpm app |
Starts the backend and UI together (backend on :8787, UI on :5173) |
pnpm compile:default |
Compile input/user.vson → output/schema.json |
pnpm gen |
Regenerate lexer/parser (run manually if you change grammars; requires Java). Not used by other scripts. |
pnpm check |
Full validation: build, test, compile:default, conflicts, strict TS. No Java — scripts do not run or require Java. |
pnpm backend |
Starts the backend locally on port 8787 |
pnpm ui |
Starts the frontend on http://localhost:5173/ |
Note: All scripts are cross-platform and work on Windows, macOS, and Linux.
| Task | Files |
|---|---|
| Add new token/keyword | src/grammars/VSONLexer.g4 → pnpm gen |
| Change grammar rules | src/grammars/VSONParser.g4 → pnpm gen |
| Add new AST node | src/ast/ast.ts, src/ast/visitor.ts |
| Change how AST is built | src/parser/parser.ts (buildAST function) |
| Change evaluation logic | src/evaluator/evaluation-visitor.ts |
| Change JSON Schema output | src/evaluator/generator.ts |
| Add new built-in type | src/builtin/builtin.ts |
| Add CLI commands | src/cli.ts, src/index.ts |
| Add mutable operations | src/ast/ast.ts, src/evaluator/evaluation-visitor.ts |
| Category | Choice | Version |
|---|---|---|
| Package Manager | pnpm | 10.x |
| Language | TypeScript | 5.x |
| Parser Generator | antlr4ts | 0.5.0-alpha.4 |
| ANTLR Runtime | antlr4ts | 0.5.0-alpha.4 |
| Testing | Jest + ts-jest | 29.x |
| Dev Runtime | tsx | 4.x |
| Module System | ESM | - |
Note: Lexer and parser use only native TypeScript and antlr4ts — no Java at runtime. Scripts do not run or require Java. All scripts (
scripts/*.ts) are in TypeScript and run via tsx. To regenerate lexer/parser after grammar changes, runpnpm genmanually (that command uses Java via antlr4ts-cli).pnpm checkworks the same on Windows, macOS, and Linux.
- Composition is inlined (no JSON Schema
$ref); each schema is a standalone object - No circular schema references
- No schema imports from other files
- Only
has()is supported inside a conditional statement (eg.if (has(...) .... )) - Conditional only supports at most two operands (eg.
if (A && B)is supported, butif (A && B && C)is not)
- Inline optional fields (
field: type @optional) - Mutable state operations (
add,addOptional,remove) - Array types (
tags: string[],float[], etc.) - Conditionals with
has(),&&,||,!(including nesting) - Custom error reporting with line/column info
- Validation UI with live editing
- Schema composition (
address: Address,waitlist: User[]) - JSON Schema
$refoutput (optional) - Ajv validation integration