54

I'm using the default JSON serialization for a model that has a number of decimal and integer attributes. An example result is:

{ "user": { "id": 1234, "rating": "98.7" } }

Notice the addition of quotes around the value of "rating". This causes the deserialization library I'm using to incorrectly treat these as strings (instead of decimals). Can Rails be set to not use the quotes for all decimals?

Edit:

I'm on Rails 3.0.7 and Ruby 1.9.2 if that makes a difference.

Edit:

Terminal:

rails g model user rating:decimal
rake db:migrate

Console:

user = User.create(rating: 98.7)
user.to_json
Kevin Sylvestre
  • 37,288
  • 33
  • 152
  • 232

4 Answers4

55

The only "safe" way to hand decimals from language A to language B is to use a String. If your json contains "rating": 98.79999999999999 it will probably be converted to 98.79999999999998 by your JavaScript runtime.

See BigDecimal as_json documentation:

A BigDecimal would be naturally represented as a JSON number. Most libraries, however, parse non-integer JSON numbers directly as floats. Clients using those libraries would get in general a wrong number and no way to recover other than manually inspecting the string with the JSON code itself.

That’s why a JSON string is returned. The JSON literal is not numeric, but if the other end knows by contract that the data is supposed to be a BigDecimal, it still has the chance to post-process the string and get the real value.

If you want to force Rails not to quote these, you could monkey-patch BigDecimal (see Rails source).

# not needed: to compare with the Numeric implementation
class Numeric
  def as_json(options = nil) self end #:nodoc:
  def encode_json(encoder) to_s end #:nodoc:
end

class BigDecimal
  def as_json(options = nil) self end
  def encode_json(encoder) to_s end #:nodoc:
end
Marcel Jackwerth
  • 53,948
  • 9
  • 74
  • 88
  • Thanks Marcel. Should I not be using 'decimal' type for storage of currencies and volumes? I don't think I'll ever hit any serious rounding errors. – Kevin Sylvestre May 26 '11 at 19:25
  • 1
    In theory you should. But I would prefer integers over decimals (the `money` gem uses integers in a `xyz_cents` column) for most real-world problems regarding currencies. – Marcel Jackwerth May 26 '11 at 21:21
29

This has changed for Rails 4.0 which has the option ActiveSupport.encode_big_decimal_as_string so that you can specify your BigDecimal serialization preference. See issue 6033

In the meantime, if you're comfortable with the arguments put forward in 6033 and you're running a Rails version lower than 4.0 you can monkey patch BigDecimal as below

require 'bigdecimal'

class BigDecimal
  def as_json(options = nil) #:nodoc:
    if finite?
      self
    else
      NilClass::AS_JSON
    end
  end
end

This solved my issues with RABL pumping out strings for dollar amounts stored as BigDecimal.

toxaq
  • 6,745
  • 3
  • 46
  • 56
  • Thanks for the update! I haven't looked at this issue in a bit but it is good to know it can be customized in 4.0 – Kevin Sylvestre Jul 07 '13 at 05:41
  • No problem. Just thought I'd put this solution out there for others doing the rounds on Google! – toxaq Jul 07 '13 at 06:02
  • 5
    Unfortunately the `encode_big_decimal_as_string` option has been deprecated again in Rails 4.1. – Jazz Oct 28 '14 at 22:10
  • 2
    Aparently you can use `activesupport-json_encoder` gem to restore it – Waiting for Dev... Dec 11 '14 at 07:26
  • 1
    making breaking changes is a great idea <\sarcasm> – Tilo Nov 06 '15 at 18:38
  • @Tilo I don't understand the intention of your sarcasm. Breaking changes happen all the time. Your tests should confirm that your app conforms to it's requirements. – toxaq Nov 07 '15 at 02:14
  • 1
    sorry for the sarcasm, but I wasn't exactly happy to waste precious time on figuring out why decimals are encoded as strings. When those minutes are multiplied by a large number of Rails users, maybe that's a good reason for not doing breaking changes like this. You build model rockets? Cool!! – Tilo Nov 07 '15 at 17:16
  • Oh I see, I thought you meant my solution was a breaking change :) Yes, I'm not sure what the thoughts are on this from the Rails team. Technically valid for share trading apps where fractions matter, not so helpful for the more common use-case of product prices! – toxaq Nov 08 '15 at 00:59
  • Thanks for your answer. However, didn't work for me as is. Added `self.to_f` instead of `self` - this worker ok. – Sergey Mell Mar 07 '17 at 20:46
10

if you are using ActiveModel::Serializer you can also use to_f to force the conversion from Decimal to Float type. that wil also trim out the quote for you!

so in your object serializer class. do

def rating
  self.rating.to_f
end
house9
  • 20,359
  • 8
  • 55
  • 61
alexzg
  • 845
  • 8
  • 8
  • 4
    Doesn't this completely ruin the point of using BigDecimal in the first place, though? If I'm trying to pass a monetary value to an API endpoint, for example, this approach will re-introduce all of the rounding errors inherent to Floats. If it mutilates the data, it shouldn't be considered a solution. – Jazz Oct 28 '14 at 22:07
  • 5
    This causes "SystemStackError (stack level too deep)" exception for me. Using object.rating.to_f works fine. – Nestor Turizo Aug 24 '17 at 04:36
0

With Rails 5 encode_big_decimal_as_string doesn't work (it was deprecated so no surprise there).

If you add jbuilder to your application

# Gemfile
gem 'jbuilder', '~> 2.5'

Then just create a json view that casts the decimal to a float just for the view, you should be golden.

# app/views/yourmodel/index.json.jbuilder
json.array! @yourmodels do |yourmodel|
  json.attributethatisadecimal yourmodel.attributethatisadecimal.to_f
end

This worked well for me - a bit more work (because you have to map your model to jbuilder) but this approach seems pretty clean.

jfgrissom
  • 592
  • 5
  • 13