Search code examples
jsoncrystal-lang

Is there a more idiomatic way to cast json to a specific Hash type in Crystal?


I've got a JSON::Any, and when I access the "params" key I get a JSON::Any representing this:

{"a":0.9029593355545088,"b":2,"lh":1000,"ph":10,"pl":1}

I'd like to turn this into a Hash(String, Float64) and the simplest way I've found to do this is using to_json and then from_json since Hash#from_json doesn't accept the JSON::Any type.

params = Hash(String, Float64).from_json(json["params"].to_json)

It seems like I'm missing a concept or idiom somewhere. Is there a better way?


PS

If you want to play around with this, here's a snippet:

require "json"
json_string = %({"params": {"a": 1.2, "b": 2}})
json = JSON.parse(json_string)
Hash(String, Float64).from_json(json["params"].to_json)

Please note that in my code I don't have such easy access to json_string. Because it's buried in a much larger json response, I have to parse it to get down there and once parsed I have a JSON::Any. Thus it would seem I need to turn it back into a json string before I can turn it into a hash using from_json.


Solution

  • JSON.parse returns a JSON::Any, so you could use its handy casting helpers:

    json["params"].as_h.transform_values(&.raw.as(Float64|Int64).to_f)
    

    However if the structure is well defined, it's much better to make use of JSON::Serializable:

    record ApiResponse, params : Hash(String, Float64) do
      include JSON::Serializable
    end
    
    json_string = %({"params": {"a": 1.2, "b": 2}})
    json = ApiResponse.from_json(json_string)
    json.params # => {"a" => 1.2, "b" => 2.0}
    

    Or even better yet:

    record ApiResponse, params : ApiParams do
      include JSON::Serializable
    end
    
    record ApiParams, a : Float64, b : Float64 do
      include JSON::Serializable
    end
    
    json_string = %({"params": {"a": 1.2, "b": 2}})
    json = ApiResponse.from_json(json_string)
    json.params # => ApiParams(@a=1.2, @b=2.0)