Currently JSON is inconvenient to use, not sure if that's a bug or feature. The perfectly reasonable code below would fail complaining Int can't be cast to Float
require "json"
JSON.parse("{\"v\":1}")["v"].as_f
Expected behaviour: The code above should work and JSON::Any should allow reasonable automatic casting between Int -> Float, Number -> String etc.
How I hit this bug - I tried to parse JSON like { a: 1, b: 1.1 } - when some values casted to Int and some to Float and the only way to handle it is to json.to_i? || json.to_f which feels wrong.
FWIW a better approach would be to deserialize the data into structs/classes via JSON::Serializable or JSON.mapping. This also works for literal types.
Hash(String, Float64).from_json "{\"v\":1}"
In my opinion having JSON::Any is fine, it's easy to understand and use. Especially if working with dynamic and messy JSON (like in my case), I don't want to properly parse it, I want to take only the data I want and being able to play with it dynamically.
There should probably be to_i and to_f in addition to as_i and as_f, which are fuzzy on whether the value is an integer or float.
Why not use as_i.to_f? It makes it clear that you are converting an int to float, which might result in precision loss.
@asterite because that will fail if the value is 1.0, since it's a json float.
@alexeypetrushin is asking how to treat 1 and 1.0 in json both as floats, for libraries which don't make the distinction.
There is no issue on behavior, the JSON document is represented correctly.
What is requested is to simplify casting, which is usually better to be explicit in my opinion.
Converting types to another can be already done by using #raw.
For example, converting all types to String will be #raw.to_s. In your case, you can use #raw.as(Float64 | Int64).to_f.
What is this API we are talking about? I don't understand why sometimes it's an int, sometimes a float. And the OP already provided a solution so I'm not sure there's something to do here.
The #raw.as(Float64 | Int64).to_f should work.
@asterite I'm parsing historical price data, it's like [{ date: "2020-01-01", price: 500 }, { date: "2020-01-02", price: 521.2 }, ...] } JSON parses numbers sometimes as Int and sometimes as Float which makes it hard to parse.
json.as_a.each do |row|
prices << Float64.new(row.as_h["price"].as_i? || row.as_h["price"].as_f)
end
What is this API we are talking about?
Make JSON::Any.to_f to also auto-cast integers instead of throwing exception. Not insisting, feel free to close issue if disagree.
If the value is an explicit float (1.0) then Any will parse it as a Float64, if it's ambiguous (1) the type will be an Int64, because it looks like an integer.
The Any#as_i is basically doing .as(Int64) and same for as_f (.as(Float64)). It's a basic type cast with no transformation. If Any parsed an Int64 (1) then trying to cast a Float64 out of the JSON::Type union will fail.
There are workarounds, as explained above, for example using manual casts with an explicit transformation of the raw union, like .raw.as(Int64 | Float64).to_f. That will always return a float, but that's not very pretty nor friendly to use :(
Maybe there could be explicit Any#to_f and Any#to_i methods that would do that for Float64 and Int64 (and maybe even String) automatically? I think it would fit into Any's purpose: parse whatever inefficiently but simply and nicely.
Sounds good. I guess to_f would call to_f on a string if the value is a string? Same for to_i? But then to_i returns Int64 I guess?
Or maybe as_f could just cast the int to a float if it's an int, and that's it.
Or maybe
as_fcould just cast the int to a float if it's an int, and that's it.
No. as_f should only return the raw value (if it is a float).
Adding Any#to_f and Any#to_i sounds reasonable.
Just a note that whatever is decided above could (should?) be applied to YAML::Any, as well 馃檹
Most helpful comment
Just a note that whatever is decided above could (should?) be applied to
YAML::Any, as well 馃檹