Rails jsonb - Prevent JSON keys from reordering when jsonb is saved to Postgresql database
The Postgres docs suggest using json
type to preserve the order of object keys:
In general, most applications should prefer to store JSON data as jsonb, unless there are quite specialized needs, such as legacy assumptions about ordering of object keys.
Actually they're not sorted alphabetically but rather by key length then alphabetically, that explains the order you get. The jsonb
type has been create as a better version of the json
type to write and access data and it's probably for indexation and search purpose that they change the keys order. If you want your keys order not to change, you can use the json
type that does not change the order of the keys when storing the data in the database.
Hope it helps.
You can use postgresql's json
type and preserve order. If you want to take advantage of jsonb
's many performance benefits, you lose native order preservation.
Here is one way to preserve order, by injecting a numeric index in each key:
class OrderedHashSerializer < ActiveRecord::Coders::JSON class << self def dump(obj) ActiveSupport::JSON.encode( dump_transform(obj) ) end def load(json) json = ActiveSupport::JSON.decode(json) if json.is_a?(String) load_transform(json) end private # to indicate identifiers order as the postgresql jsonb type does not preserve order: def dump_transform(obj) obj.transform_keys.with_index do |key, index| "#{index + 1}_#{key}" end end def load_transform(hash) hash &.sort { |item, next_item| item.first.to_i <=> next_item.first.to_i } &.map { |key, value| format_item(key, value) } &.to_h end def format_item(key, value) [ key.gsub(/^\d+_/, '').to_sym, value.in?([nil, true]) ? value : value.try(:to_sym) || value ] end endend
NOTE that this will undermine using embedded json data in sql queries, as all the key names will be tainted. But if you need preserve order more than you need json queries, this is one solution. (Although json
type starts to look pretty good at that point, admittedly)
Tests look like:
describe OrderedHashSerializer do describe '#load' do subject(:invoke) { described_class.load(data) } let(:data) do { '1_error' => 'checksum_failure', '2_parent' => nil, '22_last_item' => 'omega', '3_code' => 'service_server_failure', '4_demographics': { age: %w[29], 'flavor' => %w[cherry vanilla rhubarb] } }.to_json end it 'formats data properly when loading it from database' do is_expected.to eq( error: :checksum_failure, parent: nil, last_item: :omega, code: :service_server_failure, demographics: { 'age' => ["29"], 'flavor' => %w[cherry vanilla rhubarb] }, ) end it 'preserves intended key order' do expect(invoke.keys.last).to eq :last_item end end describe '#dump' do subject(:invoke) { described_class.dump(data) } let(:data) do { 'error' => 'checksum_failure', 'parent' => nil, 'code' => 'service_server_failure', demographics: { age: %w[65], 'flavor' => %w[cherry vanilla rhubarb] }, 'last_item' => 'omega' } end it 'prefixes keys with the numbers, in order' do is_expected.to eq( { "1_error" => :checksum_failure, "2_parent" => nil, "3_code" => :service_server_failure, "4_demographics" => { age: %w[65], flavor: %w[cherry vanilla rhubarb] }, "5_last_item" => :omega }.to_json ) end endend