Search code examples
ruby-on-railsjsonpostgresqlruby-on-rails-5jsonb

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


I have a column amount_splits that I need to save my JSON to in the key order I've specified.

How do I prevent Rails / Postgres jsonb from auto sorting my JSON keys when I save it to the database? (for creating or updating)

It looks like it's trying to sort alphabetically, but does a poor job at it.

Here's what I'm saving:

{
    "str_fee": 3.17,       # key 1
    "eva_fee": 14.37,      # key 2
    "fran_royalty": 14.37, # key 3
    "fran_amount": 67.09   # key 4
}

This is how it actually saves:

{
    "eva_fee": 14.37,     # key 2
    "str_fee": 3.17,      # key 1
    "fran_amount": 67.09, # key 4
    "fran_royalty": 14.37 # key 3
}

Purpose:

Before you answer "sorting doesn't matter when the JSON is consumed on the receiving end", stop and think first please... and please read on

I need the keys to be sorted in the way I need them sorted because the client interface that consumes this JSON is displaying the JSON to developers that need the keys to be in the order that the documentation tells them its in. And the reason it needs to be in that order is to display the process of what calculations happened in which order first:

The correct order tells the developer:

The str_fee was applied first, then the eva_fee, then the fran_royalty... making fran_amount the ending amount.

But based on how jsonb sorts this, it incorrectly tells our developers that:

The eva_fee was applied first, then the str_fee, then the fran_amount... making fran_royalty the ending amount.


Solution

  • [Updated on 2021/02/12] see the comment below from @mu is too short for my "accepted" answer (I'm not wanting to accept my own answer, since it's a Rails hack).

    Basically to save the order in the jsonb column, I needed to use an array (i.e. [{str_fee: 6}, {eva_fee: 11}, ...]).


    [old hacky answer]

    I can't find anything about how to modify jsonb save/update behavior, but you can control how you return your as_json from your Rails Model.

    So instead of returning your JSON by calling the self.amount_splits column directly (where it would return in the wrong key order)... manually break out each key.

    NOTE: this will only work if you know your key names ahead of time... if key names are dynamically created before you know them, you'll need to try something else... likely saving your JSON as a string instead of as a Hash.

    class Transaction < ApplicationRecord
      store_accessor :amount_splits, :str_fee, :eva_fee, :fran_royalty, :fran_amount
    
      [...]
    
      def as_json(options={})
        # simple JSON response:
        json = {
          [...]
          "amount_splits"   => {
            "str_fee"       => self.str_fee,
            "eva_fee"       => self.eva_fee,
            "fran_royalty"  => self.fran_royalty,
            "fran_amount"   => self.fran_amount
          },
          [...]
        }
        return json
      end
    
      [...]
    
    end
    

    NOTE: I've significantly abbreviated my custom as_json method, only leaving the relevant part of the JSON that it will return