Skip to content

Check duplicate keys when a non String/Symbol key follows a String/Symbol key - #1126

Merged
byroot merged 1 commit into
ruby:masterfrom
stephanmoneybird:generator-duplicate-key-check-key-order
Nov 5, 2026
Merged

byroot merged 1 commit into
ruby:masterfrom
stephanmoneybird:generator-duplicate-key-check-key-order

Conversation

@stephanmoneybird

@stephanmoneybird stephanmoneybird commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Fix: #1125

The default branch of json_object_i converted the key with to_s without the mixed keys check the T_STRING and T_SYMBOL branches have, so { "1" => 1, 1 => 2 } generated {"1":1,"1":2} while { 1 => 1, "1" => 2 } raised. It now calls json_inspect_hash_with_mixed_keys when the first key was a String or Symbol. The TruffleRuby and Java generators already catch this case.

Hashes whose keys all have the same type pay one extra comparison per non String/Symbol key. A hash that starts with a String or Symbol key and then has another key type now runs JSON.on_mixed_keys_hash once, which is the cost the reverse order already paid:

== Encoding twitter.json
              before      4.586k (± 0.9%) i/s  (218.03 μs/i)
               after      4.521k (± 0.7%) i/s  (221.17 μs/i)
== Encoding citm_catalog.json
              before      2.233k (± 0.6%) i/s  (447.78 μs/i)
               after      2.204k (± 0.5%) i/s  (453.68 μs/i)
== Encoding integer keys (1000)
              before     18.825k (± 0.9%) i/s   (53.12 μs/i)
               after     18.522k (± 1.3%) i/s   (53.99 μs/i)
== Encoding small hashes with integer keys
              before      8.031k (± 0.9%) i/s  (124.52 μs/i)
               after      7.919k (± 0.9%) i/s  (126.28 μs/i)
== Encoding string key then integer keys (1000)
              before     18.751k (± 1.3%) i/s   (53.33 μs/i)
               after      6.049k (± 1.0%) i/s  (165.33 μs/i)
== Encoding small hashes with a string key then an integer key
              before     13.111k (± 1.0%) i/s   (76.27 μs/i)
               after      3.022k (± 0.6%) i/s  (330.86 μs/i)
== Encoding integer keys then a string key (1000)
              before      5.991k (± 1.5%) i/s  (166.90 μs/i)
               after      5.837k (± 1.6%) i/s  (171.33 μs/i)
== Encoding small hashes with an integer key then a string key
              before      2.980k (± 3.0%) i/s  (335.56 μs/i)
               after      3.048k (± 1.2%) i/s  (328.05 μs/i)

The integer keys cases are (1..1000).to_h { |i| [i, i] } and Array.new(1000) { |i| { i => i, i + 1 => i } }; the mixed cases put a "a" key before or after the Integer keys. Ruby 4.0.9, arm64-darwin.

So the mixed String-then-Integer order gets 3 to 4 times slower, the same as Integer-then-String already is. If that is too much, the cost is in JSON.on_mixed_keys_hash, which allocates a String per key and a Hash per inspected hash, and moving that check to C would speed up both orders.

A more general condition is possible as well: a single key_type != arg->first_key_type check before the switch, instead of the three per-branch checks, would also catch { 1 => :a, obj => :b } with obj.to_s == "1", which the C extension still generates while the Java and TruffleRuby generators raise. It would also inspect hashes mixing Integer and Float, or fixnums and bignums, which can never contain a duplicate. I kept the narrower check discussed in #1125, but I'm happy to switch.

@stephanmoneybird
stephanmoneybird force-pushed the generator-duplicate-key-check-key-order branch from 2918b3c to 91a2fbc Compare November 5, 2026 13:09
Comment thread ext/json/ext/generator/generator.c Resolved
@byroot
byroot force-pushed the generator-duplicate-key-check-key-order branch 2 times, most recently from 5d9ac7e to 0153951 Compare November 5, 2026 17:29
…mbol key

Fix: #1125

Co-Authored-By: Jean Boussier <byroot@ruby-lang.org>
@byroot
byroot force-pushed the generator-duplicate-key-check-key-order branch from 0153951 to 2e9d3e9 Compare November 5, 2026 17:37
@byroot
byroot merged commit bbb92b5 into ruby:master Nov 5, 2026
42 checks passed
@stephanmoneybird
stephanmoneybird deleted the generator-duplicate-key-check-key-order branch November 5, 2026 17:51
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.

Generator duplicate key check depends on key order with non String/Symbol keys

2 participants