How to Add a Key-Value Pair in Nested Hashes or Json?

An abstract problem is presented where you have a hash structure as follows:

a = {
  "b" => [
    {
      "c" => {
        "d" => "as_x",
      },
      "f" => 0,
      "g" => "ok",
    },
  ],
  "h" => "re",
}

The task is to add a key-value pair “e” => “as_y” at the same level as “d” within the hash, resulting in a new hash. While this can be easily achieved in Ruby with the code snippet provided, it appears not as straightforward in Crystal.

a["b"][0]["c"]["e"] = "as_y"
a

Why not? What’s the problem?

The code seems to work exactly as intended in Crystal and identical to Ruby.

Sorry, I abstracted too much before, but now the result is different.

I think the tl;dr here is this is more of a “dynamic language to static language” problem than it is Ruby to Crystal. You’ll either need to do some as casts, or refactor things to not use hashes like this.

Can you share more about your use case? There’s probably a better, more Crystal way to handle it.

I am dealing with a Json response from a web service and need to add a field to it. I also tried using the JSON::Any class to handle it, but the same issue occurs: Crystal does not provide a []= method for JSON::Any. Now my option is to recreate a hash variable and copy the values into it. So, is there any better solution?

Have you looked into JSON::Serializable - Crystal 1.11.2 at all? If the structure of the JSON is known, you can model it, then would be able to work in a more OOP style with more strictly typed objects.

So, when I want to even read a value like a[“b”][0][“c”], I have to create a class and then parse this hash into the class to read it, right? Is there a simpler method?

No, as called out in the doc you’d create a type to represent the data, then can do like MyObj.from_json json_string_or_io. From there could possibly do something like my_obj.b[0].c. But will depend on what the JSON is like, how you want the API to be…

https://app.quicktype.io/ supports Crystal so could try throwing the JSON into there and have it generate the types.

1 Like

One more question, please look at this example. Why would the to_json method not work for the two variables of the same type?

Run #glmq | Compile & run code in Crystal (carc.in)

I think the root cause is that Char doesn’t seem to implement the proper to_json method. I searched around for a bit and couldn’t find an issue/PR for this so likely was just missed.

However to answer your question, you can see the problem more clearly by doing:

typeof(a["b"][0]) # => (Char | Hash(String, Hash(String, String) | Int32))

Because the type of a is like Hash(String, String | Hash(Hash(...)) the compiler can’t know what will actually be returned when you do a["b"]. I.e. will it be a Hash or a String? However it just so happens both Hash and String respond to the #[] method. Because of this, the compiler can figure out that the type of a["b"][0] will either be the nested Hash type, or a Char. The latter comes from the String#[] method that returns a Char at the provided index, which in this case would be the first char.

If you change the h key to false you’ll now see it just doesn’t compile because there is no Bool#[] method.

1 Like