javascript
exercises
exercises.js⚡javascript
/**
* ========================================
* 11.2 Working with JSON - Exercises
* ========================================
*
* Practice JSON parsing, stringifying, and manipulation.
* Complete each exercise by filling in the code.
*/
/**
* EXERCISE 1: Basic JSON Conversion
*
* Convert between JavaScript objects and JSON strings.
*/
function objectToJSON(obj) {
// Convert object to JSON string
}
function jsonToObject(jsonString) {
// Convert JSON string to object
// Return null if invalid JSON
}
// Test:
// objectToJSON({ name: 'John', age: 30 }) → '{"name":"John","age":30}'
// jsonToObject('{"name":"John"}') → { name: 'John' }
// jsonToObject('invalid') → null
/**
* EXERCISE 2: Pretty Print JSON
*
* Create a function that formats JSON with custom indentation.
*/
function prettyJSON(obj, indent = 2) {
// Return prettified JSON string
}
// Test:
// prettyJSON({ a: 1, b: 2 }, 4) should return nicely formatted string
/**
* EXERCISE 3: Filter Sensitive Data
*
* Create a function that removes sensitive fields from JSON.
*/
function sanitizeJSON(obj, sensitiveFields = ['password', 'ssn', 'token']) {
// Return JSON string with sensitive fields removed
}
// Test:
// sanitizeJSON({ user: 'john', password: 'secret' })
// → '{"user":"john"}'
/**
* EXERCISE 4: Date-Aware JSON
*
* Create stringify/parse functions that handle dates properly.
*/
function stringifyWithDates(obj) {
// Stringify object, dates should become ISO strings
}
function parseWithDates(jsonString) {
// Parse JSON, converting ISO date strings back to Date objects
}
// Test:
// const obj = { event: 'party', date: new Date('2024-12-31') };
// const json = stringifyWithDates(obj);
// const parsed = parseWithDates(json);
// parsed.date instanceof Date → true
/**
* EXERCISE 5: JSON Path Accessor
*
* Access nested values using dot notation path.
*/
function getJSONPath(obj, path) {
// Get value at path (e.g., 'user.address.city')
// Return undefined if path doesn't exist
}
function setJSONPath(obj, path, value) {
// Set value at path, creating nested objects if needed
// Return modified object
}
// Test:
// getJSONPath({ user: { name: 'John' } }, 'user.name') → 'John'
// setJSONPath({}, 'user.address.city', 'NYC')
// → { user: { address: { city: 'NYC' } } }
/**
* EXERCISE 6: Safe Circular JSON
*
* Stringify objects with circular references.
*/
function safeStringify(obj, space = 2) {
// Stringify even with circular references
// Replace circular refs with '[Circular]'
}
// Test:
// const obj = { name: 'test' };
// obj.self = obj;
// safeStringify(obj) should not throw
/**
* EXERCISE 7: JSON Schema Validator
*
* Validate JSON against a schema.
*/
function validateSchema(data, schema) {
// Return { valid: boolean, errors: string[] }
// Schema format:
// {
// fieldName: {
// type: 'string' | 'number' | 'boolean' | 'array' | 'object',
// required: boolean,
// min: number (for numbers/strings/arrays),
// max: number,
// pattern: RegExp (for strings)
// }
// }
}
// Test:
// const schema = {
// name: { type: 'string', required: true, min: 2 },
// age: { type: 'number', required: true, min: 0, max: 150 }
// };
// validateSchema({ name: 'J', age: 200 }, schema)
// → { valid: false, errors: [...] }
/**
* EXERCISE 8: JSON Merge
*
* Deep merge multiple JSON objects.
*/
function mergeJSON(...objects) {
// Deep merge objects (later objects override earlier)
// Arrays should be replaced, not merged
}
// Test:
// mergeJSON(
// { a: 1, b: { c: 2 } },
// { b: { d: 3 }, e: 4 }
// )
// → { a: 1, b: { c: 2, d: 3 }, e: 4 }
/**
* EXERCISE 9: JSON Diff
*
* Find differences between two objects.
*/
function diffJSON(obj1, obj2) {
// Return array of differences:
// [{ path: string, type: 'added' | 'removed' | 'changed', oldValue?, newValue? }]
}
// Test:
// diffJSON({ a: 1, b: 2 }, { a: 1, b: 3, c: 4 })
// → [
// { path: 'b', type: 'changed', oldValue: 2, newValue: 3 },
// { path: 'c', type: 'added', newValue: 4 }
// ]
/**
* EXERCISE 10: JSON Flattener
*
* Flatten nested objects to single-level with dot notation keys.
*/
function flattenJSON(obj, prefix = '') {
// Flatten nested object
// { a: { b: { c: 1 } } } → { 'a.b.c': 1 }
}
function unflattenJSON(flatObj) {
// Reverse: { 'a.b.c': 1 } → { a: { b: { c: 1 } } }
}
// Test:
// flattenJSON({ user: { name: 'John', address: { city: 'NYC' } } })
// → { 'user.name': 'John', 'user.address.city': 'NYC' }
/**
* EXERCISE 11: Collection Serializer
*
* Serialize/deserialize Maps and Sets.
*/
function serializeCollections(obj) {
// Stringify with Map/Set support
}
function deserializeCollections(json) {
// Parse with Map/Set restoration
}
// Test:
// const obj = { myMap: new Map([['a', 1]]), mySet: new Set([1,2,3]) };
// const json = serializeCollections(obj);
// const restored = deserializeCollections(json);
// restored.myMap instanceof Map → true
/**
* EXERCISE 12: JSON Query
*
* Query JSON like a database.
*/
function queryJSON(data, query) {
// data: array of objects
// query: { field: value } or { field: { $gt: n, $lt: n, $in: [] } }
// Return matching items
}
// Test:
// const users = [
// { name: 'Alice', age: 25 },
// { name: 'Bob', age: 30 },
// { name: 'Charlie', age: 35 }
// ];
// queryJSON(users, { age: { $gt: 25 } }) → [Bob, Charlie]
// queryJSON(users, { name: { $in: ['Alice', 'Bob'] } }) → [Alice, Bob]
/**
* EXERCISE 13: JSON Transformer
*
* Transform JSON structure using a mapping.
*/
function transformJSON(obj, mapping) {
// mapping: { newKey: 'old.path' } or { newKey: (obj) => value }
// Transform object according to mapping
}
// Test:
// transformJSON(
// { user: { firstName: 'John', lastName: 'Doe' }, age: 30 },
// {
// fullName: obj => `${obj.user.firstName} ${obj.user.lastName}`,
// userAge: 'age',
// city: 'user.address.city' // undefined if doesn't exist
// }
// )
// → { fullName: 'John Doe', userAge: 30, city: undefined }
/**
* EXERCISE 14: JSON Patch
*
* Apply JSON Patch operations (RFC 6902 simplified).
*/
function applyJSONPatch(obj, patches) {
// patches: array of { op: 'add'|'remove'|'replace', path: string, value?: any }
// Apply patches to object and return result
}
// Test:
// applyJSONPatch(
// { name: 'John', age: 30 },
// [
// { op: 'replace', path: '/name', value: 'Jane' },
// { op: 'add', path: '/email', value: 'jane@example.com' },
// { op: 'remove', path: '/age' }
// ]
// )
// → { name: 'Jane', email: 'jane@example.com' }
/**
* EXERCISE 15: Streaming JSON Parser
*
* Parse large JSON-like data incrementally.
*/
class JSONStreamParser {
constructor() {
this.buffer = '';
this.objects = [];
}
write(chunk) {
// Add chunk to buffer
// Try to extract complete JSON objects
// Return array of parsed objects
}
end() {
// Flush any remaining data
// Return final parsed objects or throw if incomplete
}
}
// Test:
// const parser = new JSONStreamParser();
// parser.write('{"name":"Jo');
// parser.write('hn"}{"age":');
// parser.write('30}');
// parser.end();
// → [{ name: 'John' }, { age: 30 }]
// ============================================
// SOLUTIONS (Hidden - Scroll to reveal)
// ============================================
/*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* SOLUTIONS BELOW
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
// SOLUTION 1: Basic JSON Conversion
function objectToJSONSolution(obj) {
return JSON.stringify(obj);
}
function jsonToObjectSolution(jsonString) {
try {
return JSON.parse(jsonString);
} catch {
return null;
}
}
// SOLUTION 2: Pretty Print JSON
function prettyJSONSolution(obj, indent = 2) {
return JSON.stringify(obj, null, indent);
}
// SOLUTION 3: Filter Sensitive Data
function sanitizeJSONSolution(
obj,
sensitiveFields = ['password', 'ssn', 'token']
) {
return JSON.stringify(obj, (key, value) => {
if (sensitiveFields.includes(key)) {
return undefined;
}
return value;
});
}
// SOLUTION 4: Date-Aware JSON
function stringifyWithDatesSolution(obj) {
return JSON.stringify(obj);
}
function parseWithDatesSolution(jsonString) {
const isoDateRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
return JSON.parse(jsonString, (key, value) => {
if (typeof value === 'string' && isoDateRegex.test(value)) {
return new Date(value);
}
return value;
});
}
// SOLUTION 5: JSON Path Accessor
function getJSONPathSolution(obj, path) {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (current === null || current === undefined) {
return undefined;
}
current = current[part];
}
return current;
}
function setJSONPathSolution(obj, path, value) {
const parts = path.split('.');
let current = obj;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!(part in current) || typeof current[part] !== 'object') {
current[part] = {};
}
current = current[part];
}
current[parts[parts.length - 1]] = value;
return obj;
}
// SOLUTION 6: Safe Circular JSON
function safeStringifySolution(obj, space = 2) {
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;
},
space
);
}
// SOLUTION 7: JSON Schema Validator
function validateSchemaSolution(data, schema) {
const errors = [];
for (const [field, rules] of Object.entries(schema)) {
const value = data[field];
// Required check
if (rules.required && (value === undefined || value === null)) {
errors.push(`${field} is required`);
continue;
}
if (value === undefined || value === null) continue;
// Type check
if (rules.type) {
const actualType = Array.isArray(value) ? 'array' : typeof value;
if (actualType !== rules.type) {
errors.push(`${field} must be type ${rules.type}`);
continue;
}
}
// Min/Max for numbers
if (typeof value === 'number') {
if (rules.min !== undefined && value < rules.min) {
errors.push(`${field} must be >= ${rules.min}`);
}
if (rules.max !== undefined && value > rules.max) {
errors.push(`${field} must be <= ${rules.max}`);
}
}
// Min/Max for strings (length)
if (typeof value === 'string') {
if (rules.min !== undefined && value.length < rules.min) {
errors.push(`${field} must be at least ${rules.min} characters`);
}
if (rules.max !== undefined && value.length > rules.max) {
errors.push(`${field} must be at most ${rules.max} characters`);
}
if (rules.pattern && !rules.pattern.test(value)) {
errors.push(`${field} has invalid format`);
}
}
// Min/Max for arrays (length)
if (Array.isArray(value)) {
if (rules.min !== undefined && value.length < rules.min) {
errors.push(`${field} must have at least ${rules.min} items`);
}
if (rules.max !== undefined && value.length > rules.max) {
errors.push(`${field} must have at most ${rules.max} items`);
}
}
}
return { valid: errors.length === 0, errors };
}
// SOLUTION 8: JSON Merge
function mergeJSONSolution(...objects) {
function deepMerge(target, source) {
const result = { ...target };
for (const key of Object.keys(source)) {
const targetValue = result[key];
const sourceValue = source[key];
if (
typeof targetValue === 'object' &&
targetValue !== null &&
!Array.isArray(targetValue) &&
typeof sourceValue === 'object' &&
sourceValue !== null &&
!Array.isArray(sourceValue)
) {
result[key] = deepMerge(targetValue, sourceValue);
} else {
result[key] = sourceValue;
}
}
return result;
}
return objects.reduce((acc, obj) => deepMerge(acc, obj), {});
}
// SOLUTION 9: JSON Diff
function diffJSONSolution(obj1, obj2, path = '') {
const differences = [];
const allKeys = new Set([
...Object.keys(obj1 || {}),
...Object.keys(obj2 || {}),
]);
for (const key of allKeys) {
const fullPath = path ? `${path}.${key}` : key;
const val1 = obj1?.[key];
const val2 = obj2?.[key];
if (val1 === undefined && val2 !== undefined) {
differences.push({ path: fullPath, type: 'added', newValue: val2 });
} else if (val1 !== undefined && val2 === undefined) {
differences.push({ path: fullPath, type: 'removed', oldValue: val1 });
} else if (
typeof val1 === 'object' &&
val1 !== null &&
typeof val2 === 'object' &&
val2 !== null
) {
differences.push(...diffJSONSolution(val1, val2, fullPath));
} else if (val1 !== val2) {
differences.push({
path: fullPath,
type: 'changed',
oldValue: val1,
newValue: val2,
});
}
}
return differences;
}
// SOLUTION 10: JSON Flattener
function flattenJSONSolution(obj, prefix = '') {
const result = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
Object.assign(result, flattenJSONSolution(value, newKey));
} else {
result[newKey] = value;
}
}
return result;
}
function unflattenJSONSolution(flatObj) {
const result = {};
for (const [path, value] of Object.entries(flatObj)) {
const parts = path.split('.');
let current = result;
for (let i = 0; i < parts.length - 1; i++) {
if (!(parts[i] in current)) {
current[parts[i]] = {};
}
current = current[parts[i]];
}
current[parts[parts.length - 1]] = value;
}
return result;
}
// SOLUTION 11: Collection Serializer
function serializeCollectionsSolution(obj) {
return JSON.stringify(obj, (key, value) => {
if (value instanceof Map) {
return { __type__: 'Map', __data__: [...value.entries()] };
}
if (value instanceof Set) {
return { __type__: 'Set', __data__: [...value] };
}
return value;
});
}
function deserializeCollectionsSolution(json) {
return JSON.parse(json, (key, value) => {
if (value && typeof value === 'object') {
if (value.__type__ === 'Map') return new Map(value.__data__);
if (value.__type__ === 'Set') return new Set(value.__data__);
}
return value;
});
}
// SOLUTION 12: JSON Query
function queryJSONSolution(data, query) {
return data.filter((item) => {
for (const [field, condition] of Object.entries(query)) {
const value = item[field];
if (typeof condition === 'object' && condition !== null) {
// Handle operators
if (condition.$gt !== undefined && !(value > condition.$gt))
return false;
if (condition.$gte !== undefined && !(value >= condition.$gte))
return false;
if (condition.$lt !== undefined && !(value < condition.$lt))
return false;
if (condition.$lte !== undefined && !(value <= condition.$lte))
return false;
if (condition.$eq !== undefined && value !== condition.$eq)
return false;
if (condition.$ne !== undefined && value === condition.$ne)
return false;
if (condition.$in !== undefined && !condition.$in.includes(value))
return false;
if (condition.$nin !== undefined && condition.$nin.includes(value))
return false;
} else {
// Direct equality
if (value !== condition) return false;
}
}
return true;
});
}
// SOLUTION 13: JSON Transformer
function transformJSONSolution(obj, mapping) {
const result = {};
for (const [newKey, source] of Object.entries(mapping)) {
if (typeof source === 'function') {
result[newKey] = source(obj);
} else if (typeof source === 'string') {
result[newKey] = getJSONPathSolution(obj, source);
} else {
result[newKey] = source;
}
}
return result;
}
// SOLUTION 14: JSON Patch
function applyJSONPatchSolution(obj, patches) {
const result = JSON.parse(JSON.stringify(obj));
for (const patch of patches) {
const pathParts = patch.path.split('/').filter(Boolean);
if (patch.op === 'add' || patch.op === 'replace') {
let current = result;
for (let i = 0; i < pathParts.length - 1; i++) {
current = current[pathParts[i]];
}
current[pathParts[pathParts.length - 1]] = patch.value;
} else if (patch.op === 'remove') {
let current = result;
for (let i = 0; i < pathParts.length - 1; i++) {
current = current[pathParts[i]];
}
delete current[pathParts[pathParts.length - 1]];
}
}
return result;
}
// SOLUTION 15: Streaming JSON Parser
class JSONStreamParserSolution {
constructor() {
this.buffer = '';
this.objects = [];
}
write(chunk) {
this.buffer += chunk;
const extracted = [];
let braceCount = 0;
let startIndex = -1;
for (let i = 0; i < this.buffer.length; i++) {
const char = this.buffer[i];
if (char === '{') {
if (braceCount === 0) startIndex = i;
braceCount++;
} else if (char === '}') {
braceCount--;
if (braceCount === 0 && startIndex !== -1) {
const jsonStr = this.buffer.substring(startIndex, i + 1);
try {
extracted.push(JSON.parse(jsonStr));
this.objects.push(extracted[extracted.length - 1]);
} catch {
// Incomplete or invalid JSON
}
startIndex = -1;
}
}
}
// Keep only unparsed data in buffer
if (startIndex !== -1) {
this.buffer = this.buffer.substring(startIndex);
} else {
this.buffer = '';
}
return extracted;
}
end() {
if (this.buffer.trim()) {
try {
const obj = JSON.parse(this.buffer);
this.objects.push(obj);
} catch (e) {
throw new Error('Incomplete JSON data');
}
}
return this.objects;
}
}
console.log('Working with JSON exercises loaded!');
console.log(
'Complete each exercise and check against solutions at the bottom.'
);