Skip to content

Repository files navigation

VSON

VSON — Validation Schema Objects in Notation A DSL that compiles to JSON Schema Draft 2020-12

See it in action

Live Demo

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.


Prerequisites

  • Node.js v20 or higher (download)
  • pnpm v10 or higher

To install pnpm:

npm install -g pnpm

Quick Start

pnpm 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)

Project Structure

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

Pipeline

The compiler turns .vson source into JSON Schema in four stages:

  1. Lexing — character stream → tokens (VSONLexer)
  2. Parsing — tokens → CST → AST (VSONParser, buildAST()). Custom ErrorListener reports syntax errors with line/column info.
  3. Evaluation — AST → evaluation context (EvaluationVisitor: schema declarations, field declarations, mutate statements, conditionals with has() / && / || / !). Field types are resolved via resolveType(): built-in types, arrays (Type[]), and schema composition (references to other schemas → inline object schema).
  4. Generation — context → JSON Schema Draft 2020-12 (Generator); multiple schemas emitted when composition is used.

Diagram: pipeline.mmd (Mermaid). Rendered: Pipeline


DSL Syntax

Current Syntax

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.


Schema Composition

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.

Syntax

  • Single schema: fieldName: SchemaName; (e.g. instructor: User)
  • Array of schema: fieldName: SchemaName[]; (e.g. waitlist: User[])

Example

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 State Operations

Mutable operations allow post-declaration schema modification. They execute sequentially after the schema declaration.

Syntax

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[], …).

Example

Input:

schema User {
  name: string;
}
add(User, "age", integer);

Output:

{
  "type": "object",
  "properties": {
    "name": {"type": "string"},
    "age": {"type": "integer"}
  },
  "required": ["name", "age"]
}

JSON Schema Output Format

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"]
}

Scripts

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.vsonoutput/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.


Key Files to Modify

Task Files
Add new token/keyword src/grammars/VSONLexer.g4pnpm gen
Change grammar rules src/grammars/VSONParser.g4pnpm 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

Technology Stack

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, run pnpm gen manually (that command uses Java via antlr4ts-cli). pnpm check works the same on Windows, macOS, and Linux.


Current Limitations

  • 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, but if (A && B && C) is not)

Planned Features

  • 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 $ref output (optional)
  • Ajv validation integration

About

External DSL that compiles into JSON Schema. Pipeline includes lexing, parsing, custom AST structure and JSON Schema generation with React code editor

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages