Skip to content

Add sort_keys keyword for deterministic JSON output#442

Merged
quinnj merged 2 commits intomasterfrom
jq/sort-keys
Mar 17, 2026
Merged

Add sort_keys keyword for deterministic JSON output#442
quinnj merged 2 commits intomasterfrom
jq/sort-keys

Conversation

@quinnj
Copy link
Copy Markdown
Member

@quinnj quinnj commented Mar 17, 2026

Summary

  • Adds sort_keys::Union{Bool, Nothing}=nothing keyword argument to JSON.json / JSON.print for controlling dictionary key ordering
  • Default behavior (nothing): non-Object AbstractDict keys are sorted alphabetically; JSON.Object preserves insertion order
  • sort_keys=true: all AbstractDict types sorted, including Object
  • sort_keys=false: no sorting for any AbstractDict
  • Sorts by the lowered string representation of keys (via StructUtils.lowerkey), so works correctly with String, Symbol, Integer, and custom key types
  • Works recursively on nested dicts; composes with all existing options (pretty, omit_null, omit_empty, jsonlines, buffered IO, etc.)
  • Struct and NamedTuple field order is unaffected (already deterministic)

This is a standard feature across JSON libraries: Python's json.dumps(sort_keys=True), Go's encoding/json (always sorted), Rust's serde_json (sorted by default), Java Jackson's ORDER_MAP_ENTRIES_BY_KEYS, etc.

Rationale for default sorting

Julia's Dict has non-deterministic iteration order that varies across versions, platforms, and runs. The default sort_keys=nothing follows Go and Rust's lead by sorting Dict output by default, which eliminates a class of bugs in snapshot testing, diffing, and caching. JSON.Object is exempted because it was specifically designed for insertion-order preservation — users who choose Object have explicitly opted into a particular key ordering.

Usage

# Default: Dict sorted, Object preserves insertion order
JSON.json(Dict("c" => 3, "a" => 1))        # => {"a":1,"c":3}
JSON.json(JSON.Object("c" => 3, "a" => 1)) # => {"c":3,"a":1}

# Explicit: sort everything including Object
JSON.json(obj; sort_keys=true)

# Explicit: opt out of sorting entirely
JSON.json(dict; sort_keys=false)

Implementation

Three small changes in src/write.jl:

  1. Added sort_keys::Union{Bool, Nothing} = nothing field to WriteOptions
  2. In json!, when sorting is active and the value is an AbstractDict, collect and sort keys by lowerkey, then iterate in sorted order
  3. Propagated sort_keys through the jsonlines WriteOptions reconstruction

Closes #437

Test plan

  • Default behavior: Dict sorted, Object preserves insertion order
  • sort_keys=true: all AbstractDicts sorted including Object
  • sort_keys=false: no sorting
  • Nested Dict inside Object and vice versa (mixed behavior)
  • Empty dict, single key dict
  • Nested dicts sorted recursively
  • Symbol and Integer key types
  • Composition with pretty, omit_null, jsonlines
  • Structs, NamedTuples, arrays unaffected
  • IO and file output paths
  • Buffered IO with small buffer
  • Deeply nested mixed structures
  • Full existing test suite passes (314 tests)

🤖 Generated with Claude Code

Adds `sort_keys::Bool=false` to `JSON.json` and `JSON.print` that
produces output with alphabetically sorted dictionary keys. This is
a standard feature in JSON libraries across languages (Python's
json.dumps(sort_keys=True), Go's encoding/json, etc.) and enables
reproducible output for snapshot testing, diffing, and caching.

The implementation sorts dict keys by their lowered string
representation and works recursively for nested dicts. It correctly
handles String, Symbol, and Integer key types, JSON.Object, and
composes with all existing options (pretty, omit_null, jsonlines,
buffered IO, etc.). Struct and NamedTuple field order is unaffected
since it is already deterministic.

Closes #437

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.30%. Comparing base (f4fbb5a) to head (b700739).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #442      +/-   ##
==========================================
+ Coverage   90.26%   90.30%   +0.04%     
==========================================
  Files           7        7              
  Lines        1366     1372       +6     
==========================================
+ Hits         1233     1239       +6     
  Misses        133      133              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Change sort_keys from Bool to Union{Bool, Nothing} with default
nothing. The three-valued semantics:
- nothing (default): sort non-Object AbstractDicts, preserve Object order
- true: sort all AbstractDicts including Object
- false: no sorting for any AbstractDict

This matches Go/Rust behavior (sorted by default) while respecting
the explicit ordering contract of JSON.Object.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@quinnj quinnj merged commit de651ce into master Mar 17, 2026
11 checks passed
@quinnj quinnj deleted the jq/sort-keys branch March 17, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: sort_keys option for deterministic JSON output

1 participant