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

freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
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
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
makeitrein
Hey all, just started picking up Elixir last week and am writing a scraper as a learning project. Baby step #1 is extracting the number ...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
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

Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
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
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 27727 240
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New

We're in Beta

About us Mission Statement