JavaScript FundamentalsFeatured

JavaScript JSON: Data Interchange Format Guide

Master JSON in JavaScript. Learn parsing, stringifying, data manipulation, API integration, and best practices for working with JSON.

By JavaScriptDoc Team
jsonjavascriptdataapiparsing

JavaScript JSON: Data Interchange Format Guide

JSON (JavaScript Object Notation) is a lightweight data interchange format that's easy for humans to read and write, and easy for machines to parse and generate.

Understanding JSON

JSON is a text format that's completely language-independent but uses conventions familiar to JavaScript programmers.

// JSON data types
const jsonExamples = {
  // Strings (must use double quotes)
  string: 'Hello, World!',

  // Numbers (integer or floating point)
  integer: 42,
  float: 3.14159,

  // Booleans
  boolean: true,

  // Null
  nullValue: null,

  // Arrays
  array: [1, 2, 3, 'four', true],

  // Objects
  object: {
    name: 'John',
    age: 30,
    city: 'New York',
  },
};

// What JSON cannot contain:
// - Functions
// - undefined
// - Symbols
// - Dates (converted to strings)
// - Comments
// - Trailing commas

JSON.parse()

Basic Parsing

// Parse JSON string to JavaScript object
const jsonString = '{"name": "John", "age": 30, "city": "New York"}';
const obj = JSON.parse(jsonString);
console.log(obj.name); // "John"

// Parse array
const arrayJson = '[1, 2, 3, "four", true]';
const arr = JSON.parse(arrayJson);
console.log(arr[3]); // "four"

// Parse nested structures
const nestedJson = `{
  "user": {
    "id": 1,
    "name": "John Doe",
    "contacts": {
      "email": "john@example.com",
      "phone": "+1234567890"
    },
    "hobbies": ["reading", "gaming", "coding"]
  }
}`;

const nested = JSON.parse(nestedJson);
console.log(nested.user.contacts.email); // "john@example.com"

Parse with Reviver Function

// Reviver function to transform values during parsing
const dateJson = '{"date": "2024-03-15T10:30:00.000Z", "event": "meeting"}';

const parsed = JSON.parse(dateJson, (key, value) => {
  // Convert date strings to Date objects
  if (typeof value === 'string' && /\d{4}-\d{2}-\d{2}T/.test(value)) {
    return new Date(value);
  }
  return value;
});

console.log(parsed.date instanceof Date); // true

// Complex reviver example
const complexJson = `{
  "price": "19.99",
  "quantity": "5",
  "isActive": "true",
  "metadata": "_REMOVE_THIS_"
}`;

const complexParsed = JSON.parse(complexJson, (key, value) => {
  // Convert string numbers to numbers
  if (typeof value === 'string' && !isNaN(value)) {
    return parseFloat(value);
  }

  // Convert string booleans to booleans
  if (value === 'true') return true;
  if (value === 'false') return false;

  // Remove specific values
  if (value === '_REMOVE_THIS_') return undefined;

  return value;
});

console.log(complexParsed);
// { price: 19.99, quantity: 5, isActive: true }

Error Handling

// Safe JSON parsing
function safeParse(jsonString, defaultValue = null) {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    console.error('JSON Parse Error:', error.message);
    return defaultValue;
  }
}

// Test with invalid JSON
const invalid1 = safeParse('{invalid json}', {}); // Returns {}
const invalid2 = safeParse('undefined', null); // Returns null

// Detailed error handling
function parseWithDetails(jsonString) {
  try {
    return {
      success: true,
      data: JSON.parse(jsonString),
    };
  } catch (error) {
    // Extract error position
    const match = error.message.match(/position (\d+)/);
    const position = match ? parseInt(match[1]) : null;

    return {
      success: false,
      error: error.message,
      position,
      preview: position
        ? jsonString.substring(Math.max(0, position - 20), position + 20)
        : null,
    };
  }
}

JSON.stringify()

Basic Stringification

// Convert JavaScript object to JSON string
const obj = {
  name: 'John',
  age: 30,
  city: 'New York',
  hobbies: ['reading', 'gaming'],
};

const jsonString = JSON.stringify(obj);
console.log(jsonString);
// {"name":"John","age":30,"city":"New York","hobbies":["reading","gaming"]}

// Pretty printing with indentation
const prettyJson = JSON.stringify(obj, null, 2);
console.log(prettyJson);
/*
{
  "name": "John",
  "age": 30,
  "city": "New York",
  "hobbies": [
    "reading",
    "gaming"
  ]
}
*/

// Custom indentation
const customIndent = JSON.stringify(obj, null, '\t'); // Tab indentation
const customString = JSON.stringify(obj, null, '..'); // Custom string

Replacer Function

// Replacer function to filter/transform values
const user = {
  id: 1,
  name: 'John',
  password: 'secret123',
  email: 'john@example.com',
  createdAt: new Date(),
};

// Filter out sensitive data
const filtered = JSON.stringify(user, (key, value) => {
  if (key === 'password') return undefined;
  return value;
});

console.log(filtered);
// {"id":1,"name":"John","email":"john@example.com","createdAt":"2024-03-15T10:30:00.000Z"}

// Array as replacer (whitelist properties)
const whitelist = JSON.stringify(user, ['id', 'name', 'email']);
console.log(whitelist);
// {"id":1,"name":"John","email":"john@example.com"}

// Transform values
const transformed = JSON.stringify(user, (key, value) => {
  if (typeof value === 'string') {
    return value.toUpperCase();
  }
  if (value instanceof Date) {
    return value.toISOString().split('T')[0]; // Date only
  }
  return value;
});

Handling Special Values

// Special values in JSON.stringify
const special = {
  undefined: undefined,
  function: function () {},
  symbol: Symbol('test'),
  nan: NaN,
  infinity: Infinity,
  negInfinity: -Infinity,
  date: new Date(),
  regex: /test/gi,
  error: new Error('Test error'),
};

console.log(JSON.stringify(special));
// {"nan":null,"infinity":null,"negInfinity":null,"date":"2024-03-15T10:30:00.000Z","regex":{},"error":{}}

// Custom toJSON method
class User {
  constructor(name, email, password) {
    this.name = name;
    this.email = email;
    this.password = password;
  }

  toJSON() {
    return {
      name: this.name,
      email: this.email,
      // Password is excluded
    };
  }
}

const user = new User('John', 'john@example.com', 'secret123');
console.log(JSON.stringify(user));
// {"name":"John","email":"john@example.com"}

Working with APIs

Fetching JSON Data

// GET request for JSON data
async function fetchUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error fetching user data:', error);
    throw error;
  }
}

// POST request with JSON body
async function createUser(userData) {
  try {
    const response = await fetch('https://api.example.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(userData),
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || 'Failed to create user');
    }

    return await response.json();
  } catch (error) {
    console.error('Error creating user:', error);
    throw error;
  }
}

// Handling different response types
async function smartFetch(url) {
  const response = await fetch(url);
  const contentType = response.headers.get('content-type');

  if (contentType && contentType.includes('application/json')) {
    return await response.json();
  } else if (contentType && contentType.includes('text/')) {
    return await response.text();
  } else {
    return await response.blob();
  }
}

API Response Handling

// Response wrapper class
class ApiResponse {
  constructor(response) {
    this.ok = response.ok;
    this.status = response.status;
    this.statusText = response.statusText;
    this.headers = response.headers;
  }

  async parseJson() {
    try {
      this.data = await this.response.json();
    } catch (error) {
      this.error = 'Invalid JSON response';
      this.data = null;
    }
    return this;
  }

  hasError() {
    return !this.ok || this.error;
  }

  getError() {
    if (this.error) return this.error;
    if (!this.ok) return `HTTP ${this.status}: ${this.statusText}`;
    return null;
  }
}

// Batch API requests
async function batchFetch(urls) {
  const promises = urls.map((url) =>
    fetch(url)
      .then((res) => res.json())
      .catch((error) => ({ error: error.message, url }))
  );

  return Promise.all(promises);
}

// Retry mechanism for failed requests
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (response.ok) {
        return await response.json();
      }
      throw new Error(`HTTP ${response.status}`);
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise((resolve) =>
        setTimeout(resolve, 1000 * Math.pow(2, i))
      );
    }
  }
}

Data Validation

JSON Schema Validation

// Simple JSON validator
class JsonValidator {
  static validate(data, schema) {
    const errors = [];

    for (const [key, rules] of Object.entries(schema)) {
      if (rules.required && !(key in data)) {
        errors.push(`Missing required field: ${key}`);
        continue;
      }

      if (key in data) {
        const value = data[key];

        if (rules.type && typeof value !== rules.type) {
          errors.push(
            `Invalid type for ${key}: expected ${rules.type}, got ${typeof value}`
          );
        }

        if (rules.min !== undefined && value < rules.min) {
          errors.push(`${key} must be at least ${rules.min}`);
        }

        if (rules.max !== undefined && value > rules.max) {
          errors.push(`${key} must be at most ${rules.max}`);
        }

        if (rules.pattern && !rules.pattern.test(value)) {
          errors.push(`${key} does not match required pattern`);
        }

        if (rules.enum && !rules.enum.includes(value)) {
          errors.push(`${key} must be one of: ${rules.enum.join(', ')}`);
        }
      }
    }

    return {
      valid: errors.length === 0,
      errors,
    };
  }
}

// Usage
const userSchema = {
  name: { type: 'string', required: true, min: 2 },
  age: { type: 'number', required: true, min: 0, max: 150 },
  email: {
    type: 'string',
    required: true,
    pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
  },
  role: { type: 'string', enum: ['user', 'admin', 'moderator'] },
};

const userData = {
  name: 'John',
  age: 30,
  email: 'john@example.com',
  role: 'admin',
};

const validation = JsonValidator.validate(userData, userSchema);
console.log(validation); // { valid: true, errors: [] }

Type-Safe JSON Parsing

// Type guards for JSON data
class TypedJSON {
  static isString(value) {
    return typeof value === 'string';
  }

  static isNumber(value) {
    return typeof value === 'number' && !isNaN(value);
  }

  static isBoolean(value) {
    return typeof value === 'boolean';
  }

  static isArray(value) {
    return Array.isArray(value);
  }

  static isObject(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value);
  }

  static parseAs(jsonString, type) {
    const data = JSON.parse(jsonString);

    switch (type) {
      case 'string':
        if (!this.isString(data)) throw new TypeError('Expected string');
        break;
      case 'number':
        if (!this.isNumber(data)) throw new TypeError('Expected number');
        break;
      case 'boolean':
        if (!this.isBoolean(data)) throw new TypeError('Expected boolean');
        break;
      case 'array':
        if (!this.isArray(data)) throw new TypeError('Expected array');
        break;
      case 'object':
        if (!this.isObject(data)) throw new TypeError('Expected object');
        break;
    }

    return data;
  }

  static safeParse(jsonString, fallback = null) {
    try {
      return { success: true, data: JSON.parse(jsonString) };
    } catch (error) {
      return { success: false, error: error.message, data: fallback };
    }
  }
}

Advanced JSON Techniques

Streaming JSON

// Parse large JSON files in chunks
class JSONStream {
  constructor() {
    this.buffer = '';
    this.stack = [];
  }

  write(chunk) {
    this.buffer += chunk;
    return this.parse();
  }

  parse() {
    const results = [];
    let depth = 0;
    let inString = false;
    let start = 0;

    for (let i = 0; i < this.buffer.length; i++) {
      const char = this.buffer[i];
      const prevChar = i > 0 ? this.buffer[i - 1] : '';

      if (char === '"' && prevChar !== '\\') {
        inString = !inString;
      }

      if (!inString) {
        if (char === '{' || char === '[') {
          if (depth === 0) start = i;
          depth++;
        } else if (char === '}' || char === ']') {
          depth--;
          if (depth === 0) {
            const json = this.buffer.substring(start, i + 1);
            try {
              results.push(JSON.parse(json));
            } catch (e) {
              // Invalid JSON, skip
            }
          }
        }
      }
    }

    // Keep unparsed data in buffer
    if (depth === 0) {
      this.buffer = this.buffer.substring(start);
    }

    return results;
  }
}

// JSON Lines (JSONL) parser
function parseJSONLines(text) {
  return text
    .split('\n')
    .filter((line) => line.trim())
    .map((line) => {
      try {
        return JSON.parse(line);
      } catch (e) {
        console.error('Invalid JSON line:', line);
        return null;
      }
    })
    .filter((item) => item !== null);
}

JSON Diff and Patch

// Compare two JSON objects
function jsonDiff(obj1, obj2, path = '') {
  const changes = [];

  // Check for additions and modifications
  for (const key in obj2) {
    const newPath = path ? `${path}.${key}` : key;

    if (!(key in obj1)) {
      changes.push({ type: 'add', path: newPath, value: obj2[key] });
    } else if (typeof obj1[key] !== typeof obj2[key]) {
      changes.push({
        type: 'modify',
        path: newPath,
        oldValue: obj1[key],
        newValue: obj2[key],
      });
    } else if (typeof obj2[key] === 'object' && obj2[key] !== null) {
      if (Array.isArray(obj2[key])) {
        if (JSON.stringify(obj1[key]) !== JSON.stringify(obj2[key])) {
          changes.push({
            type: 'modify',
            path: newPath,
            oldValue: obj1[key],
            newValue: obj2[key],
          });
        }
      } else {
        changes.push(...jsonDiff(obj1[key], obj2[key], newPath));
      }
    } else if (obj1[key] !== obj2[key]) {
      changes.push({
        type: 'modify',
        path: newPath,
        oldValue: obj1[key],
        newValue: obj2[key],
      });
    }
  }

  // Check for deletions
  for (const key in obj1) {
    if (!(key in obj2)) {
      const newPath = path ? `${path}.${key}` : key;
      changes.push({ type: 'delete', path: newPath, oldValue: obj1[key] });
    }
  }

  return changes;
}

// Apply JSON patch
function applyPatch(obj, changes) {
  const result = JSON.parse(JSON.stringify(obj)); // Deep clone

  changes.forEach((change) => {
    const keys = change.path.split('.');
    const lastKey = keys.pop();
    let current = result;

    // Navigate to the parent object
    for (const key of keys) {
      if (!(key in current)) {
        current[key] = {};
      }
      current = current[key];
    }

    // Apply the change
    switch (change.type) {
      case 'add':
      case 'modify':
        current[lastKey] = change.value || change.newValue;
        break;
      case 'delete':
        delete current[lastKey];
        break;
    }
  });

  return result;
}

JSON Compression

// Simple JSON compression by removing whitespace
function compressJSON(json) {
  return JSON.stringify(JSON.parse(json));
}

// JSON minification with key mapping
class JSONCompressor {
  constructor() {
    this.keyMap = new Map();
    this.reverseMap = new Map();
    this.keyCounter = 0;
  }

  compress(obj) {
    return JSON.stringify(this.compressObject(obj));
  }

  compressObject(obj) {
    if (Array.isArray(obj)) {
      return obj.map((item) => this.compressObject(item));
    }

    if (typeof obj === 'object' && obj !== null) {
      const compressed = {};

      for (const [key, value] of Object.entries(obj)) {
        const compressedKey = this.getCompressedKey(key);
        compressed[compressedKey] = this.compressObject(value);
      }

      return compressed;
    }

    return obj;
  }

  getCompressedKey(key) {
    if (!this.keyMap.has(key)) {
      const compressed = `_${this.keyCounter++}`;
      this.keyMap.set(key, compressed);
      this.reverseMap.set(compressed, key);
    }
    return this.keyMap.get(key);
  }

  decompress(json) {
    return this.decompressObject(JSON.parse(json));
  }

  decompressObject(obj) {
    if (Array.isArray(obj)) {
      return obj.map((item) => this.decompressObject(item));
    }

    if (typeof obj === 'object' && obj !== null) {
      const decompressed = {};

      for (const [key, value] of Object.entries(obj)) {
        const originalKey = this.reverseMap.get(key) || key;
        decompressed[originalKey] = this.decompressObject(value);
      }

      return decompressed;
    }

    return obj;
  }

  getMapping() {
    return Object.fromEntries(this.keyMap);
  }
}

JSON Security

// Sanitize JSON to prevent injection attacks
class JSONSecurity {
  static sanitize(obj, maxDepth = 10, currentDepth = 0) {
    if (currentDepth > maxDepth) {
      throw new Error('Maximum depth exceeded');
    }

    if (Array.isArray(obj)) {
      return obj.map((item) => this.sanitize(item, maxDepth, currentDepth + 1));
    }

    if (typeof obj === 'object' && obj !== null) {
      const sanitized = {};

      for (const [key, value] of Object.entries(obj)) {
        // Remove potentially dangerous keys
        if (this.isDangerousKey(key)) continue;

        // Sanitize the key
        const safeKey = this.sanitizeKey(key);
        sanitized[safeKey] = this.sanitize(value, maxDepth, currentDepth + 1);
      }

      return sanitized;
    }

    // Sanitize string values
    if (typeof obj === 'string') {
      return this.sanitizeString(obj);
    }

    return obj;
  }

  static isDangerousKey(key) {
    const dangerous = ['__proto__', 'constructor', 'prototype'];
    return dangerous.includes(key.toLowerCase());
  }

  static sanitizeKey(key) {
    return key.replace(/[^a-zA-Z0-9_]/g, '_');
  }

  static sanitizeString(str) {
    return str
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#x27;')
      .replace(/\//g, '&#x2F;');
  }

  static validateJSON(jsonString, maxSize = 1024 * 1024) {
    // 1MB default
    if (jsonString.length > maxSize) {
      throw new Error('JSON string exceeds maximum size');
    }

    try {
      JSON.parse(jsonString);
      return true;
    } catch (error) {
      return false;
    }
  }
}

// Prevent JSON injection in templates
function safeJSONInject(data) {
  const json = JSON.stringify(data);
  // Escape for safe inclusion in HTML
  return json
    .replace(/</g, '\\u003c')
    .replace(/>/g, '\\u003e')
    .replace(/&/g, '\\u0026');
}

Best Practices

  1. Always validate JSON data

    function parseJSON(text) {
      try {
        return JSON.parse(text);
      } catch (e) {
        console.error('Invalid JSON:', e);
        return null;
      }
    }
    
  2. Use proper Content-Type headers

    fetch('/api/data', {
      headers: {
        'Content-Type': 'application/json',
        Accept: 'application/json',
      },
    });
    
  3. Handle circular references

    function stringify(obj) {
      const seen = new WeakSet();
      return JSON.stringify(obj, (key, value) => {
        if (typeof value === 'object' && value !== null) {
          if (seen.has(value)) {
            return '[Circular]';
          }
          seen.add(value);
        }
        return value;
      });
    }
    
  4. Consider performance for large data

    // Use streaming for large files
    // Consider compression
    // Paginate API responses
    

Conclusion

JSON is essential for modern JavaScript development:

  • Data interchange between client and server
  • Configuration files and settings
  • API communication standard
  • Data storage in various formats
  • Cross-platform compatibility

Key takeaways:

  • Always validate and sanitize JSON data
  • Handle parsing errors gracefully
  • Use appropriate methods for different scenarios
  • Consider security implications
  • Optimize for performance with large datasets
  • Follow REST API conventions

Master JSON to build robust data-driven applications!