# JSON Validator

> #!/usr/bin/env node

- Skill: `winsorllc/json-validator` (Agent Skill)
- Install (CLI): `npx skillmds@latest add winsorllc/json-validator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/winsorllc/json-validator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: winsorllc (https://skillmd.com/u/winsorllc)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/winsorllc/json-validator

---

﻿#!/usr/bin/env node
/**
 * JSON Validator - Validation and formatting tool
 */

import fs from 'fs';
import path from 'path';

// Colors
const colors = {
  reset: '\x1b[0m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  cyan: '\x1b[36m',
  bold: '\x1b[1m'
};

// Parse arguments
const args = process.argv.slice(2);
const target = args[0];
const schemaPath = args.find((_, i) => args[i-1] === '--schema');
const fix = args.includes('--fix');
const fixAll = args.includes('--fix-all');
const pretty = args.includes('--pretty');
const quiet = args.includes('--quiet');

function printHeader() {
  if (quiet) return;
  console.log(colors.cyan + 'ΓòÉ'.repeat(64) + colors.reset);
  console.log(colors.bold + '                     JSON VALIDATION REPORT                     ' + colors.reset);
  console.log(colors.cyan + 'ΓòÉ'.repeat(64) + colors.reset);
  console.log();
}

function formatBytes(bytes) {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}

function getLineAndColumn(content, position) {
  const lines = content.substring(0, position).split('\n');
  return {
    line: lines.length,
    column: lines[lines.length - 1].length + 1
  };
}

function validateJSON(filePath, schema = null) {
  const content = fs.readFileSync(filePath, 'utf8');
  const stats = fs.statSync(filePath);
  
  try {
    const parsed = JSON.parse(content);
    
    let result = {
      valid: true,
      file: filePath,
      size: formatBytes(stats.size),
      lines: content.split('\n').length
    };
    
    // Schema validation (basic)
    if (schema) {
      result.schemaValid = true; // Simplified - full schema validation would need ajv
    }
    
    return result;
  } catch (error) {
    const pos = getLineAndColumn(content, error.message.match(/position (\d+)/)?.[1] || 0);
    return {
      valid: false,
      file: filePath,
      size: formatBytes(stats.size),
      error: error.message,
      line: pos.line,
      column: pos.column
    };
  }
}

function fixJSON(filePath) {
  const content = fs.readFileSync(filePath, 'utf8');
  
  try {
    const parsed = JSON.parse(content);
    const fixed = JSON.stringify(parsed, null, 2);
    
    if (fixed !== content) {
      fs.writeFileSync(filePath, fixed);
      return { fixed: true, file: filePath };
    }
    return { fixed: false, file: filePath };
  } catch (error) {
    return { fixed: false, file: filePath, error: error.message };
  }
}

function prettyPrint(filePath) {
  const content = fs.readFileSync(filePath, 'utf8');
  const parsed = JSON.parse(content);
  console.log(JSON.stringify(parsed, null, 2));
}

function getJSONFiles(dir, files = []) {
  if (!fs.existsSync(dir)) return files;
  
  if (fs.statSync(dir).isFile()) {
    if (path.extname(dir) === '.json') {
      files.push(dir);
    }
    return files;
  }
  
  const items = fs.readdirSync(dir, { withFileTypes: true });
  
  for (const item of items) {
    if (item.name === 'node_modules' || item.name === '.git') continue;
    
    const fullPath = path.join(dir, item.name);
    if (item.isDirectory()) {
      getJSONFiles(fullPath, files);
    } else if (path.extname(item.name) === '.json') {
      files.push(fullPath);
    }
  }
  
  return files;
}

// Main
if (!target) {
  console.error(colors.red + 'Usage: validate.js <file.json|directory> [options]' + colors.reset);
  console.log();
  console.log('Options:');
  console.log('  --schema <file>    Validate against JSON schema');
  console.log('  --fix              Fix formatting of single file');
  console.log('  --fix-all          Fix formatting of all files');
  console.log('  --pretty           Pretty print the JSON');
  console.log('  --quiet            Only show errors');
  process.exit(1);
}

if (!fs.existsSync(target)) {
  console.error(colors.red + `Error: Path not found: ${target}` + colors.reset);
  process.exit(1);
}

// Handle pretty print
if (pretty) {
  try {
    prettyPrint(target);
    process.exit(0);
  } catch (e) {
    console.error(colors.red + `Error: ${e.message}` + colors.reset);
    process.exit(1);
  }
}

// Handle fix single file
if (fix) {
  if (fs.statSync(target).isDirectory()) {
    console.error(colors.red + 'Error: --fix requires a file, not a directory' + colors.reset);
    process.exit(1);
  }
  
  const result = fixJSON(target);
  if (result.error) {
    console.error(colors.red + `Cannot fix ${result.file}: ${result.error}` + colors.reset);
    process.exit(1);
  }
  if (result.fixed) {
    console.log(colors.green + `Γ£ô Fixed formatting: ${result.file}` + colors.reset);
  } else {
    console.log(colors.blue + `ΓÇó Already formatted: ${result.file}` + colors.reset);
  }
  process.exit(0);
}

// Get list of files
const files = fs.statSync(target).isFile() ? [target] : getJSONFiles(target);

if (files.length === 0) {
  console.error(colors.yellow + 'No JSON files found' + colors.reset);
  process.exit(0);
}

// Load schema if specified
let schema = null;
if (schemaPath) {
  try {
    schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
  } catch (e) {
    console.error(colors.red + `Error loading schema: ${e.message}` + colors.reset);
    process.exit(1);
  }
}

printHeader();

let validCount = 0;
let errorCount = 0;

for (const file of files) {
  const result = validateJSON(file, schema);
  
  if (result.valid) {
    validCount++;
    if (!quiet) {
      console.log(`${colors.green}Γ£ô${colors.reset} ${result.file}`);
      console.log(`  Valid JSON - ${result.size}, ${result.lines} lines`);
      console.log();
    }
  } else {
    errorCount++;
    console.error(`${colors.red}Γ£ù${colors.reset} ${result.file}`);
    console.error(`  ${colors.red}${result.error}${colors.reset}`);
    console.error(`  Line ${result.line}, Column ${result.column}`);
    console.error();
  }
}

// Handle fix-all
if (fixAll) {
  console.log(colors.cyan + 'Fixing all JSON files...' + colors.reset);
  for (const file of files) {
    const fixResult = fixJSON(file);
    if (fixResult.fixed) {
      console.log(`${colors.green}Γ£ô${colors.reset} Fixed: ${fixResult.file}`);
    } else if (fixResult.error) {
      console.log(`${colors.red}Γ£ù${colors.reset} Cannot fix: ${fixResult.file} - ${fixResult.error}`);
    }
  }
  console.log();
}

// Summary
if (!quiet) {
  console.log(colors.cyan + 'ΓöÇ'.repeat(64) + colors.reset);
  console.log(`Total: ${files.length} files (${colors.green}${validCount} valid${colors.reset}, ${colors.red}${errorCount} error${colors.reset})`);
  console.log(colors.cyan + 'ΓòÉ'.repeat(64) + colors.reset);
}

process.exit(errorCount > 0 ? 1 : 0);

