惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

MongoDB | Blog
MongoDB | Blog
罗磊的独立博客
美团技术团队
B
Blog
量子位
The Cloudflare Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
aimingoo的专栏
aimingoo的专栏
The GitHub Blog
The GitHub Blog
博客园 - 聂微东
P
Proofpoint News Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
D
DataBreaches.Net
博客园 - 三生石上(FineUI控件)
Y
Y Combinator Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Vercel News
Vercel News
Blog — PlanetScale
Blog — PlanetScale
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
阮一峰的网络日志
阮一峰的网络日志
S
SegmentFault 最新的问题

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
I Built a Browser-Only JSON Schema Validator — Draft-07, ...
Dev Nestio · 2026-06-28 · via DEV Community

Dev Nestio

I Built a Browser-Only JSON Schema Validator — Draft-07, $ref, allOf/anyOf/oneOf, if/then/else, 173 Tests

JSON Schema validation usually means pulling in ajv or a similar library. That's entirely reasonable for production code — but for quick schema checking, debugging, or learning how Draft-07 keywords work, you don't want to spin up a Node project just to paste in some JSON.

So I built a zero-dependency browser tool that implements the Draft-07 spec from scratch.


What it validates

The tool implements a solid subset of JSON Schema Draft-07:

Keyword group Keywords
Type system type (string, number, integer, boolean, array, object, null, arrays of types)
Enumeration enum, const
String minLength, maxLength, pattern, format
Number minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf
Array items (schema + tuple), additionalItems, minItems, maxItems, uniqueItems, contains
Object required, properties, additionalProperties, patternProperties, minProperties, maxProperties, dependencies, propertyNames
Combining allOf, anyOf, oneOf, not
Conditionals if / then / else
References $ref (local: #/$defs/..., #/definitions/...)
Formats email, uri, date, date-time, ipv4, uuid

Every error includes:

  • JSON path to the failing value (e.g. user.address.zipCode)
  • Human-readable message (e.g. String does not match pattern "^\d{5}$")
  • Keyword badge so you know exactly which schema rule failed

Architecture: pure recursive validator

The entire validator is a single recursive function validate(data, schema, path, rootSchema). It takes a value, a schema object, the current path string, and the root schema (for $ref resolution), and returns an array of ValidationError objects.

/**
 * @typedef {{ path: string, message: string, keyword: string }} ValidationError
 */

function validate(data, schema, path, rootSchema) {
  path = path || '';
  rootSchema = rootSchema || schema;

  if (schema === true) return [];
  if (schema === false) return [{ path, message: 'Schema is false', keyword: 'false schema' }];

  const errors = [];

  // $ref short-circuits all other keywords (Draft-07 spec)
  if (schema.$ref) {
    const resolved = resolveRef(schema.$ref, rootSchema);
    if (resolved) return validate(data, resolved, path, rootSchema);
    return [{ path, message: `Cannot resolve $ref "${schema.$ref}"`, keyword: '$ref' }];
  }

  // Type check first — no point checking string keywords on a number
  if (schema.type !== undefined && !typeMatches(data, schema.type)) {
    const expected = Array.isArray(schema.type) ? schema.type.join(' | ') : schema.type;
    return [{ path, message: `Expected "${expected}" but got "${getType(data)}"`, keyword: 'type' }];
  }

  if (typeof data === 'string') errors.push(...validateString(data, schema, path));
  if (typeof data === 'number') errors.push(...validateNumber(data, schema, path));
  if (Array.isArray(data))     errors.push(...validateArray(data, schema, path, rootSchema));
  if (isPlainObject(data))     errors.push(...validateObject(data, schema, path, rootSchema));

  // Combiners
  if (schema.allOf) schema.allOf.forEach(s => errors.push(...validate(data, s, path, rootSchema)));
  if (schema.anyOf && !schema.anyOf.some(s => validate(data, s, path, rootSchema).length === 0))
    errors.push({ path, message: 'Does not match any schema in "anyOf"', keyword: 'anyOf' });
  // ...oneOf, not, if/then/else similarly

  return errors;
}

The early return on type mismatch is important: there's no value in reporting "minLength" requires 5 characters when the value isn't even a string.


Handling exclusiveMinimum/Maximum across drafts

Draft-04 uses boolean exclusiveMinimum / exclusiveMaximum paired with minimum / maximum. Draft-06/07 changed them to be standalone numbers. The validator handles both:

function validateNumber(val, schema, path) {
  const errors = [];

  if (typeof schema.minimum === 'number') {
    if (schema.exclusiveMinimum === true) {
      // Draft-04 style
      if (val <= schema.minimum)
        errors.push({ path, message: `${val} must be > ${schema.minimum}`, keyword: 'exclusiveMinimum' });
    } else {
      if (val < schema.minimum)
        errors.push({ path, message: `${val} must be >= ${schema.minimum}`, keyword: 'minimum' });
    }
  }

  // Draft-06/07 style: exclusiveMinimum as a number
  if (typeof schema.exclusiveMinimum === 'number') {
    if (val <= schema.exclusiveMinimum)
      errors.push({ path, message: `${val} must be > ${schema.exclusiveMinimum}`, keyword: 'exclusiveMinimum' });
  }

  // ... same pattern for maximum / exclusiveMaximum

  return errors;
}

This means schemas from older tooling still work correctly.


$ref resolution

Local $ref values like #/$defs/Address or #/definitions/UUID are resolved by walking the JSON pointer path from the root schema:

function resolveRef(ref, rootSchema) {
  if (!ref.startsWith('#')) return null;  // external refs not supported
  const fragment = ref.slice(1);
  if (fragment === '' || fragment === '/') return rootSchema;  // bare # → root
  const parts = fragment.slice(1)
    .split('/')
    .map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));  // JSON Pointer unescaping
  let cur = rootSchema;
  for (const part of parts) {
    if (cur === null || typeof cur !== 'object') return null;
    cur = cur[part];
  }
  return cur !== undefined ? cur : null;
}

Per the Draft-07 spec, when a schema has a $ref, all sibling keywords are ignored — $ref short-circuits everything else.


if/then/else

One of the more useful Draft-07 additions is conditional validation:

if (schema.if !== undefined) {
  const ifErrors = validate(data, schema.if, path, rootSchema);
  if (ifErrors.length === 0) {
    // condition passed → apply then
    if (schema.then !== undefined)
      errors.push(...validate(data, schema.then, path, rootSchema));
  } else {
    // condition failed → apply else
    if (schema.else !== undefined)
      errors.push(...validate(data, schema.else, path, rootSchema));
  }
}

This lets you write schemas like "if the type field is 'credit_card', then cardNumber is required" without splitting into separate schemas.


Testing: 173 tests, zero frameworks

The same vm-based extraction technique I use across all DevNest.io tools:

const vm = require('vm');
const modObj = { exports: {} };
vm.runInContext(extractedScript, vm.createContext({
  module: modObj, exports: modObj.exports,
  window: { addEventListener: () => {} },
  document: { getElementById: () => ({}), querySelectorAll: () => [], addEventListener: () => {} },
  Date, JSON, RegExp, Number, Math, Array, Object, String, Boolean, console
}));

const { validate, getType, typeMatches, isValidEmail, ... } = modObj.exports;

Tests cover:

Function Tests
getType 10 — null, boolean, integer vs number, string, array, object
typeMatches 15 — single types, type arrays, integer/number coercion
isValidEmail 10 — valid addresses, missing parts, spaces, double @
isValidUri 8 — schemes, missing scheme, bad scheme start
isValidDate 10 — valid dates, leap years, out-of-range months/days
isValidDateTime 8 — ISO 8601, offsets, milliseconds, invalid formats
isValidIPv4 8 — valid IPs, out-of-range octets, wrong part count
isValidUUID 8 — v1/v4, wrong variant, no hyphens
validateString 15 — minLength, maxLength, pattern, all formats
validateNumber 15 — min/max, exclusive variants (bool + number), multipleOf
validateArray 12 — minItems, maxItems, uniqueItems, items schema, tuple, additionalItems, contains
validateObject 15 — required, properties, additionalProperties, minProperties, maxProperties, patternProperties, dependencies
resolveRef 8 — $defs, definitions, # root, ~1 escaping
validate (integration) 30 — all major keywords end-to-end

All 173 pass with node test/test.js.


UI design decisions

Split pane layout: JSON on the left, Schema on the right, results spanning the full width below. Ctrl+Enter triggers validation from either pane.

Parse errors are shown per-pane: if your JSON is malformed, you see the parse error under the JSON pane immediately — before even clicking Validate.

Error cards show three pieces of information: the JSON path (in monospace, highlighted), the human message, and the failing keyword as a chip. This mirrors how ajv formats errors, making it easier to cross-reference against the spec.

Three built-in examples: User (a well-formed object), Product (nested object + pattern properties), and Invalid (the User schema with deliberate violations) — so you can explore the tool immediately without having to write a schema from scratch.


Try it

json-schema-validator-dev.pages.dev — free, no login, no tracking.

Part of devnestio.pages.dev — a growing collection of browser-only developer tools. Currently 27 tools and counting.