Just added: Algorithms you can run and practice
Updated Jul 16, 2026 Edit on GitHub

Minify and Extend

Pretty-printing makes JSON readable. Sometimes you want the opposite - the smallest possible string to send over the wire or stuff into a config field. This phase adds a minify mode, then a key-sorting option that makes output stable enough to diff, and finishes with directions to keep going after the weekend's over.

Minify is the same call, minus the indent

You already know how to minify. It's JSON.stringify with no indent argument. Parse to clean out whatever spacing was there, stringify with no spacing to get the tightest valid form.

Guess roughly how many characters shorter the minified version will be before you run it and see the real number.

function minify(text) {
  return JSON.stringify(JSON.parse(text));
}

const spaced = `{
  "name": "Ada",
  "langs": [ "Pascal", "Ada" ],
  "active": true
}`;

const small = minify(spaced);
console.log(small);
console.log("from", spaced.length, "chars down to", small.length);

Run it. The multi-line input collapses to one line and you'll see the character count drop. Parsing first matters - it guarantees the output is valid JSON, not a clumsy find-and-replace on whitespace that would mangle spaces inside string values.

One function, both directions

There's no reason to keep two functions. A mode argument picks pretty or compact, and both share the same parse.

function convert(text, mode = "pretty") {
  const data = JSON.parse(text);
  if (mode === "minify") return JSON.stringify(data);
  return JSON.stringify(data, null, 2);
}

const raw = '{"id":7,"tags":["x","y"]}';

console.log("--- pretty ---");
console.log(convert(raw, "pretty"));
console.log("--- minify ---");
console.log(convert(raw, "minify"));

Run it and watch the same input come out two ways. That's your formatter and your minifier in one place.

Sorting keys for stable output

Here's a problem you hit the moment you try to compare two JSON files: object key order. {"a":1,"b":2} and {"b":2,"a":1} are the same data, but as text they're different lines, and any diff tool will light up red. The fix is to sort keys before stringifying, so the same data always produces the same text.

That third argument to JSON.stringify - the replacer we skipped in Phase 1 - has a second form worth knowing about. We'll use it here, alongside the function form from a moment ago.

Your turn. sortedFormat is the point of this phase - the thing that makes two files holding the same data produce identical text. Have a go before you read on. Fill it in and hit Run: the checks underneath tell you whether it works. My version is in the next block whenever you want it.

function sortedFormat(text, indent = 2) {
  // Parse `text`, then stringify it with every key across the whole
  // structure written in alphabetical order (not just the top level).
  // Use `indent` spaces per level. Return the formatted string.
  //
  // Hint: JSON.stringify's second argument (the replacer) can be either
  // a function called for every key/value, or an array of key names to
  // include (in that order). You get to use both forms here.
}

// --- checks: fix your function until this prints "All good." ---
const messy = '{"name":"Ada","age":36,"admin":true,"name2":"x"}';
const out = sortedFormat(messy);
if (typeof out !== "string") {
  throw new Error(`sortedFormat should return a string, got: ${JSON.stringify(out)}`);
}

const keysInOrder = [...out.matchAll(/"(\w+)":/g)].map((m) => m[1]);
const expectedOrder = ["admin", "age", "name", "name2"];
if (JSON.stringify(keysInOrder) !== JSON.stringify(expectedOrder)) {
  throw new Error(`keys should come out alphabetically as ${JSON.stringify(expectedOrder)}, got: ${JSON.stringify(keysInOrder)}`);
}

const parsed = JSON.parse(out);
if (parsed.name !== "Ada" || parsed.age !== 36 || parsed.admin !== true || parsed.name2 !== "x") {
  throw new Error(`sorting keys should not change the values, got: ${out}`);
}

const nested = '{"b":{"z":1,"a":2},"a":1}';
const nestedOut = sortedFormat(nested);
const nestedKeys = [...nestedOut.matchAll(/"(\w+)":/g)].map((m) => m[1]);
if (JSON.stringify(nestedKeys) !== JSON.stringify(["a", "b", "a", "z"])) {
  throw new Error(`sorting should reach into nested objects too, got: ${JSON.stringify(nestedKeys)}`);
}

console.log("All good.");

Stuck on reaching the nested keys too? The function-form replacer runs once for every key anywhere in the structure, not just the top-level ones - that's how you can collect all of them in a single pass.

One way to write it

function sortedFormat(text, indent = 2) {
  const data = JSON.parse(text);
  // Gather every key across the whole structure, sorted.
  const keys = new Set();
  JSON.stringify(data, (key, value) => {
    if (key) keys.add(key);
    return value;
  });
  const ordered = [...keys].sort();
  return JSON.stringify(data, ordered, indent);
}

const messy = '{"name":"Ada","age":36,"admin":true,"name2":"x"}';

console.log("--- as written ---");
console.log(JSON.stringify(JSON.parse(messy), null, 2));
console.log("--- keys sorted ---");
console.log(sortedFormat(messy));

That second form: pass the replacer an array of keys and it outputs only those keys, in that order - hand it the sorted list of every key and you get sorted output. We use the replacer-as-function form once to collect all keys, then the replacer-as-array form to emit them in sorted order. Run it. The first block keeps the original key order; the second alphabetizes every key. Now two files holding the same data, formatted this way, produce identical text - and a diff between them shows only real differences.

The whole tool, assembled

Here's everything from all four phases in one place - parse, pretty-print, explain errors, check shape, minify, sort. This is the thing you set out to build.

function typeOf(v) {
  if (v === null) return "null";
  if (Array.isArray(v)) return "array";
  return typeof v;
}

function checkShape(data, shape) {
  const problems = [];
  if (typeOf(data) !== "object") return ["Expected an object, got " + typeOf(data)];
  for (const key of Object.keys(shape)) {
    if (!(key in data)) problems.push('Missing "' + key + '"');
    else if (typeOf(data[key]) !== shape[key]) {
      problems.push('"' + key + '" should be ' + shape[key] + ", got " + typeOf(data[key]));
    }
  }
  return problems;
}

function jsonTool(text, { mode = "pretty", sort = false, shape = null } = {}) {
  let data;
  try {
    data = JSON.parse(text);
  } catch (err) {
    return { ok: false, stage: "parse", message: err.message };
  }

  if (shape) {
    const problems = checkShape(data, shape);
    if (problems.length) return { ok: false, stage: "shape", problems };
  }

  let replacer = null;
  if (sort) {
    const keys = new Set();
    JSON.stringify(data, (k, v) => (k && keys.add(k), v));
    replacer = [...keys].sort();
  }

  const indent = mode === "minify" ? undefined : 2;
  return { ok: true, output: JSON.stringify(data, replacer, indent) };
}

const raw = '{"name":"Ada","age":36,"admin":true}';

console.log(jsonTool(raw)); // pretty
console.log(jsonTool(raw, { mode: "minify" }));
console.log(jsonTool(raw, { sort: true }));
console.log(jsonTool(raw, { shape: { name: "string", age: "number" } }));
console.log(jsonTool('{"name":"Ada","age":"36"}', { shape: { name: "string", age: "number" } }));
console.log(jsonTool('{"name":}', {})); // broken

Run it. One function, one options object, every behavior we built. Pretty by default, minify on request, sorted on request, shape-checked when you hand it a shape, and a clean error report when the text won't parse. That's a real tool.

Where to take it next

You've got a working formatter and validator. Here are directions worth a future afternoon - each builds on what's already here, none needs a library.

Idea The seed
Diff two JSONs Parse both, walk keys, report added / removed / changed
Nested shape check Let a shape value be another shape and recurse
Highlight Wrap strings, numbers, keys in spans for colour in a page
Sort + diff combo Sort both inputs first so the diff shows only real changes

The diff is the most fun, so here's a starting sketch - a shallow compare of two flat objects that reports what changed.

Before you run it, count by eye: how many keys change value between before and after, and how many are brand new?

function diff(a, b) {
  const changes = [];
  const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
  for (const key of keys) {
    if (!(key in a)) changes.push("added " + key + " = " + JSON.stringify(b[key]));
    else if (!(key in b)) changes.push("removed " + key);
    else if (JSON.stringify(a[key]) !== JSON.stringify(b[key])) {
      changes.push("changed " + key + ": " + JSON.stringify(a[key]) + " -> " + JSON.stringify(b[key]));
    }
  }
  return changes;
}

const before = { name: "Ada", age: 36, admin: false };
const after = { name: "Ada", age: 37, admin: true, email: "[email protected]" };

console.log(diff(before, after));

Run it. You get age changed, admin changed, and email added - a small, clear diff. Notice it uses JSON.stringify to compare values, the same trick that lets it tell [1,2] apart from [1,3] without writing a deep-equality function. Make it recurse into nested objects and you've got something genuinely useful.

That's the build. You started with a one-line mess and ended with a tool that formats it, tells you when it's broken and where, checks it's the data you meant, shrinks it back down, and can even tell you what changed between two versions - every line of which you understand, because you ran all of it yourself.