How to Sort JSON Arrays and Objects by Any Field

Try the Related Tool …/a>

Sorting JSON arrays is essential for organizing data for display, analysis, and comparison. JavaScript's Array.prototype.sort() is deceptively simple—its default string-based comparison produces unintuitive results for numbers, and sorting deeply nested object arrays requires understanding compare functions, stability guarantees, and locale-aware string ordering.

How JavaScript sort() Actually Works

Array.prototype.sort() converts elements to strings and compares their UTF-16 code unit sequences by default. This is why [3, 1, 10, 2].sort() produces [1, 10, 2, 3]—"10" comes before "2" lexicographically. Always provide a compare function for numeric sorting. The compare function should return a negative value if a should come before b, positive if b should come before a, and zero if they are equal. Modern JavaScript engines use TimSort (a hybrid of merge sort and insertion sort), which is stable—elements that compare as equal maintain their original relative order.

Sorting Primitives

// Numeric sort — ALWAYS provide a compare function
[3, 1, 4, 1, 5].sort((a, b) => a - b);
// [1, 1, 3, 4, 5]

// Locale-aware string sort
['café', 'apple', 'Banana'].sort((a, b) => a.localeCompare(b, 'en', {sensitivity: 'base'}));
// ['apple', 'Banana', 'café'] — case-insensitive, accent-aware

Sorting Arrays of Objects by Single Field

const users = [
  { name: 'Alice', age: 30 },
  { name: 'Bob', age: 25 },
  { name: 'Carol', age: 35 }
];

users.sort((a, b) => a.age - b.age);        // ascending by age
users.sort((a, b) => b.age - a.age);        // descending by age
users.sort((a, b) => a.name.localeCompare(b.name)); // alphabetical by name

Multi-Field Sorting

Chain comparisons with the OR operator (||). Each comparison runs only if the previous ones tie:

// Sort by department, then by name within each department
employees.sort((a, b) => 
  a.department.localeCompare(b.department) || a.name.localeCompare(b.name)
);

// Sort by priority (desc), then by creation date (asc) for tie-breaking
tasks.sort((a, b) => 
  b.priority - a.priority || new Date(a.created) - new Date(b.created)
);

// Sort by status order, then by timestamp
const STATUS_ORDER = { 'active': 0, 'pending': 1, 'archived': 2 };
records.sort((a, b) =>
  STATUS_ORDER[a.status] - STATUS_ORDER[b.status] || 
  new Date(a.updatedAt) - new Date(b.updatedAt)
);

Sorting by Date, Nested, and Optional Fields

// ISO date strings — construct Date objects in compare function
events.sort((a, b) => new Date(a.date) - new Date(b.date));

// Nested fields with null safety
products.sort((a, b) => {
  const priceA = a.specs?.price ?? Infinity;
  const priceB = b.specs?.price ?? Infinity;
  return priceA - priceB;
});

// Custom sort orders with lookup maps
const PRIORITY = { 'critical': 0, 'high': 1, 'medium': 2, 'low': 3 };
tickets.sort((a, b) => PRIORITY[a.severity] - PRIORITY[b.severity]);

Performance: toSorted vs sort and Large Arrays

sort() mutates the original array. Use toSorted() (ES2023) to return a new sorted array without modifying the original. Both have O(n log n) time complexity with V8's TimSort implementation, but toSorted() adds O(n) memory overhead for the copy. For arrays under 5,000 items, the performance difference is negligible. For larger datasets, prefer sort() if you don't need the original order. For extremely large datasets (100K+), consider pre-extracting sort keys with map() before sorting to avoid repeated property access inside the compare function—this Schwartzian transform can reduce sort time by 40-60%.

Sorting JSON Object Keys for Deterministic Output

JavaScript object property order is: integer-like string keys in ascending numeric order, then remaining string keys in insertion order. For deterministic JSON output (consistent hashing, reproducible diffs), reconstruct objects with sorted keys:

function sortObjectKeys(obj) {
  if (Array.isArray(obj)) return obj.map(sortObjectKeys);
  if (typeof obj !== 'object' || obj === null) return obj;
  return Object.keys(obj).sort().reduce((sorted, key) => {
    sorted[key] = sortObjectKeys(obj[key]);
    return sorted;
  }, {});
}
// JSON.stringify(sortObjectKeys(data), null, 2)

Natural Sorting for Mixed Alphanumeric Strings

Standard string sorting places "item10" before "item2" because character-by-character comparison puts '1' before '2'. Natural sorting (also called human sorting or alphanumeric sorting) parses numeric substrings as numbers, ordering "item2" before "item10". JavaScript's Intl.Collator with the numeric: true option provides this natively: arr.sort(new Intl.Collator('en', {numeric: true, sensitivity: 'base'}).compare). This handles version strings (v1.10.0 after v1.2.0), file names with counters, and any mixed alphanumeric identifiers. For environments without Intl.Collator, the natural-compare package provides equivalent functionality with a simple naturalCompare(a, b) function. The edge case most developers miss: when sorting alphanumeric strings with different number counts, like ["a1b2", "a1b10"], a naive natural sort will compare by segment pairs, which may produce surprising orderings. Test with representative data samples before deploying.

Stable Sort and the TimSort Internals

Since ES2019, JavaScript's Array.prototype.sort() is guaranteed stable across all engines. Stability means that elements with equal sort keys maintain their original relative order. This enables chained sorting: sort by secondary key first, then by primary key, and the secondary ordering is preserved within primary key groups. V8's TimSort implementation identifies naturally ordered subsequences ("runs") in the input data and merges them—this means nearly-sorted data sorts in O(n) time. When sorting JSON arrays of objects, pre-group your data if possible (e.g., sort within departments before a full-sort), as TimSort will detect and leverage existing ordering. The worst case for TimSort is reverse-ordered data, which degrades to O(n log n) but still benefits from merge sort's cache-friendly sequential access pattern compared to quicksort's random access.

Rounding Out: Collation and International Sorting

For proper international text sorting, always use localeCompare() instead of raw string comparison operators. The sensitivity option controls accent and case handling: 'base' treats 'a' = 'á' = 'A' = 'Ä', 'accent' distinguishes accents but ignores case, and 'variant' distinguishes everything. The caseFirst option controls whether uppercase or lowercase sorts first. For German phonebooks, localeCompare('de', {sensitivity: 'base'}) correctly treats 'ä' as 'ae' for sorting. For Swedish, the same option would sort 'ä' after 'z' because Swedish treats it as a distinct letter. Getting collation wrong produces bugs that native speakers notice immediately—a French user seeing "cote, côte, coté, côté" sorted as "cote, coté, côte, côté" will recognize the sorting as broken because diacritics in French sort from the end of the word backward.

You May Also Like