Rails jsonb - Prevent JSON keys from reordering when jsonb is saved to Postgresql database Rails jsonb - Prevent JSON keys from reordering when jsonb is saved to Postgresql database postgresql postgresql

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