Maxximiliann

Maxximiliann

Reusing atomized JSON binary keys

This proof of concept periodically polls data from a specific API before decoding the JSON. It should create a list of atoms from the binary keys it receives the first time it queries the API for reuse with each subsequent poll.

defmodule ProofOfConcept do
  def naive_string_to_atom(raw_json) do
    {:ok, decoded_api_response} = Jason.decode(raw_json)

    root_keys = 
    decoded_api_response
    |> List.first()
    |> Map.keys()

    nested_keys =
      Enum.flat_map(
        root_keys,
        fn key ->
          decoded_api_response
          |> Enum.map(fn map_element ->
            foo = Map.get(map_element, key)

            case is_map(foo) do
              true ->
                Map.keys(foo)

              false ->
                :not_a_key
            end
          end)
          |> Enum.reject(&(&1 == :not_a_key))
        end
      )
      |> Enum.flat_map(& &1)
      |> Enum.uniq()

    
      extracted_keys = 
      (nested_keys ++ root_keys)
      |> Enum.map(&String.to_atom(&1))

      IO.inspect(extracted_keys, limit: :infinity)
  end

  def decode(raw_json) do
    try do
      Jason.decode(raw_json, keys: :atoms!)
    rescue
      _ ->
        IO.puts("Converting JSON keys to atoms . . . ")
        naive_string_to_atom(raw_json)
        main()
    end
  end

  def main() do
    api_url = "https://wavescap.com/api/assets.json"
    {:ok, api_response} = HTTPoison.get(api_url)

    decode(api_response.body)
  end
end

The problem:
Apparently the atomized keys are never retained in memory, requiring them to be recreated each time updated data is retrieved from the API.

iex(1)> ProofOfConcept.main
Converting JSON keys to atoms . . . 
[:firstPrice_brln, :firstPrice_btc, :firstPrice_cnyn, :firstPrice_eth,
 :firstPrice_eurn, :firstPrice_gbpn, :firstPrice_jpyn, :firstPrice_ngnn,
 :firstPrice_rubn, :firstPrice_tryn, :firstPrice_uahn, :"firstPrice_usd-n",
 :firstPrice_waves, :lastPrice_brln, :lastPrice_btc, :lastPrice_cnyn,
 :lastPrice_eth, :lastPrice_eurn, :lastPrice_gbpn, :lastPrice_jpyn,
 :lastPrice_ngnn, :lastPrice_rubn, :lastPrice_tryn, :lastPrice_uahn,
 :"lastPrice_usd-n", :lastPrice_waves, :"24h_vol_brln", :"24h_vol_btc",
 :"24h_vol_cnyn", :"24h_vol_eth", :"24h_vol_eurn", :"24h_vol_gbpn",
 :"24h_vol_jpyn", :"24h_vol_ngnn", :"24h_vol_rubn", :"24h_vol_tryn",
 :"24h_vol_uahn", :"24h_vol_usd-n", :"24h_vol_waves", :circulating, :data,
 :excludedFromCirculating, :id, :name, :precision, :sender, :shortcode, :start,
 :totalSupply, :trades, :website]

Converting JSON keys to atoms . . . 
[:firstPrice_brln, :firstPrice_btc, :firstPrice_cnyn, :firstPrice_eth,
 :firstPrice_eurn, :firstPrice_gbpn, :firstPrice_jpyn, :firstPrice_ngnn,
 :firstPrice_rubn, :firstPrice_tryn, :firstPrice_uahn, :"firstPrice_usd-n",
 :firstPrice_waves, :lastPrice_brln, :lastPrice_btc, :lastPrice_cnyn,
 :lastPrice_eth, :lastPrice_eurn, :lastPrice_gbpn, :lastPrice_jpyn,
 :lastPrice_ngnn, :lastPrice_rubn, :lastPrice_tryn, :lastPrice_uahn,
 :"lastPrice_usd-n", :lastPrice_waves, :"24h_vol_brln", :"24h_vol_btc",
 :"24h_vol_cnyn", :"24h_vol_eth", :"24h_vol_eurn", :"24h_vol_gbpn",
 :"24h_vol_jpyn", :"24h_vol_ngnn", :"24h_vol_rubn", :"24h_vol_tryn",
 :"24h_vol_uahn", :"24h_vol_usd-n", :"24h_vol_waves", :circulating, :data,
 :excludedFromCirculating, :id, :name, :precision, :sender, :shortcode, :start,
 :totalSupply, :trades, :website]

Converting JSON keys to atoms . . . 
[:firstPrice_brln, :firstPrice_btc, :firstPrice_cnyn, :firstPrice_eth,
 :firstPrice_eurn, :firstPrice_gbpn, :firstPrice_jpyn, :firstPrice_ngnn,
 :firstPrice_rubn, :firstPrice_tryn, :firstPrice_uahn, :"firstPrice_usd-n",
 :firstPrice_waves, :lastPrice_brln, :lastPrice_btc, :lastPrice_cnyn,
 :lastPrice_eth, :lastPrice_eurn, :lastPrice_gbpn, :lastPrice_jpyn,
 :lastPrice_ngnn, :lastPrice_rubn, :lastPrice_tryn, :lastPrice_uahn,
 :"lastPrice_usd-n", :lastPrice_waves, :"24h_vol_brln", :"24h_vol_btc",
 :"24h_vol_cnyn", :"24h_vol_eth", :"24h_vol_eurn", :"24h_vol_gbpn",
 :"24h_vol_jpyn", :"24h_vol_ngnn", :"24h_vol_rubn", :"24h_vol_tryn",
 :"24h_vol_uahn", :"24h_vol_usd-n", :"24h_vol_waves", :circulating, :data,
 :excludedFromCirculating, :id, :name, :precision, :sender, :shortcode, :start,
 :totalSupply, :trades, :website]

Converting JSON keys to atoms . . . 
[:firstPrice_brln, :firstPrice_btc, :firstPrice_cnyn, :firstPrice_eth,
 :firstPrice_eurn, :firstPrice_gbpn, :firstPrice_jpyn, :firstPrice_ngnn,
 :firstPrice_rubn, :firstPrice_tryn, :firstPrice_uahn, :"firstPrice_usd-n",
 :firstPrice_waves, :lastPrice_brln, :lastPrice_btc, :lastPrice_cnyn,
 :lastPrice_eth, :lastPrice_eurn, :lastPrice_gbpn, :lastPrice_jpyn,
 :lastPrice_ngnn, :lastPrice_rubn, :lastPrice_tryn, :lastPrice_uahn,
 :"lastPrice_usd-n", :lastPrice_waves, :"24h_vol_brln", :"24h_vol_btc",
 :"24h_vol_cnyn", :"24h_vol_eth", :"24h_vol_eurn", :"24h_vol_gbpn",
 :"24h_vol_jpyn", :"24h_vol_ngnn", :"24h_vol_rubn", :"24h_vol_tryn",
 :"24h_vol_uahn", :"24h_vol_usd-n", :"24h_vol_waves", :circulating, :data,
 :excludedFromCirculating, :id, :name, :precision, :sender, :shortcode, :start,
 :totalSupply, :trades, :website]

Converting JSON keys to atoms . . . 
[:firstPrice_brln, :firstPrice_btc, :firstPrice_cnyn, :firstPrice_eth,
 :firstPrice_eurn, :firstPrice_gbpn, :firstPrice_jpyn, :firstPrice_ngnn,
 :firstPrice_rubn, :firstPrice_tryn, :firstPrice_uahn, :"firstPrice_usd-n",
 :firstPrice_waves, :lastPrice_brln, :lastPrice_btc, :lastPrice_cnyn,
 :lastPrice_eth, :lastPrice_eurn, :lastPrice_gbpn, :lastPrice_jpyn,
 :lastPrice_ngnn, :lastPrice_rubn, :lastPrice_tryn, :lastPrice_uahn,
 :"lastPrice_usd-n", :lastPrice_waves, :"24h_vol_brln", :"24h_vol_btc",
 :"24h_vol_cnyn", :"24h_vol_eth", :"24h_vol_eurn", :"24h_vol_gbpn",
 :"24h_vol_jpyn", :"24h_vol_ngnn", :"24h_vol_rubn", :"24h_vol_tryn",
 :"24h_vol_uahn", :"24h_vol_usd-n", :"24h_vol_waves", :circulating, :data,
 :excludedFromCirculating, :id, :name, :precision, :sender, :shortcode, :start,
 :totalSupply, :trades, :website]

After each new API poll, why aren’t the atoms that were previously generated being reused?

Marked As Solved

Maxximiliann

Maxximiliann

The API is not universally constructed, as it turns out, so some records have keys that are absent from others. I was able to deduce what was going wrong and rewrite the code to accommodate for it thanks to being able to examine the error stacktrace. Thanks for the tip!

Btw, please elaborate on a few of the points you made here when you have a moment to spare, thanks again!

Also Liked

al2o3cr

al2o3cr

If one of the elements of decoded_api_response has additional keys not present in the first one, naive_string_to_atom will not see them and the bug you’re seeing would happen.

The error message here should be helpful - on OTP 25, it is explicit about what string failed:

iex(1)> Jason.decode("{\"24h\":0}", keys: :atoms!)
** (ArgumentError) errors were found at the given arguments:

  * 1st argument: not an already existing atom

    :erlang.binary_to_existing_atom("24h", :utf8)
    (elixir 1.13.4) lib/string.ex:2456: String.to_existing_atom/1
    (jason 1.3.0) lib/decoder.ex:322: Jason.Decoder.object/6
    (jason 1.3.0) lib/decoder.ex:55: Jason.Decoder.parse/2
al2o3cr

al2o3cr

The message in the ArgumentError is only the top half of the output from triggering that error in iex - the other half is from __STACKTRACE__:

iex(4)> try do                                      
...(4)>   Jason.decode("{\"24h\":0}", keys: :atoms!)
...(4)> rescue                                      
...(4)>   error ->                                  
...(4)>     __STACKTRACE__
...(4)> end
warning: variable "error" is unused (if the variable is not meant to be used, prefix it with an underscore)
  iex:7

[
  {:erlang, :binary_to_existing_atom, ["24h", :utf8],
   [error_info: %{module: :erl_erts_errors}]},
  {String, :to_existing_atom, 1, [file: 'lib/string.ex', line: 2456]},
  {Jason.Decoder, :object, 6, [file: 'lib/decoder.ex', line: 322]},
  {Jason.Decoder, :parse, 2, [file: 'lib/decoder.ex', line: 55]},
  {:erl_eval, :do_apply, 7, [file: 'erl_eval.erl', line: 744]},
  {:erl_eval, :try_clauses, 10, [file: 'erl_eval.erl', line: 987]},
  {:elixir, :recur_eval, 3, [file: 'src/elixir.erl', line: 296]},
  {:elixir, :eval_forms, 3, [file: 'src/elixir.erl', line: 274]}
]

Where Next?

Popular in Questions Top

pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
LegitStack
I’m hoping you guys can give me some general advice and perhaps code examples if you’re feeling up to it. I’m very interested in Elixir,...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
pgiesin
This should be a simple problem but I just can’t seem to figure it out. I have a standalone Elixir app that won’t find the database. Dep...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
jc00ke
Expanding on this topic: https://forum.elixirforum.net/t/map-typespec-question/19217 Let’s say I have a map with required and optional k...
New
idi527
I’ve been re-reading swift book again and noticed that multiline strings there don’t have a trailing line break, unlike in elixir iex(2)...
New

Other popular topics Top

chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
yurko
Here are few pieces of (common) Linux knowledge that we use for reasonably small one server apps. We use Ubuntu but this should work for ...
New
William
I would like to know that is there any online source for learning Phoenix Framework for building E-Commerce Store? Any advantage on build...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
script
If I have a string “1000 cfu/ml” . I want to remove the characters and / and space . So the string is like this "1000" What is the ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

We're in Beta

About us Mission Statement