Skip to content

Examples

Eric Pailleau edited this page Aug 7, 2018 · 20 revisions

Examples

Convert JSON object to record specification

Create a record definition file from a JSON sample :

1> A = jason:decode_file("priv/ex1.json",[{mode, record}, {to, "/tmp/records.hrl"}]).
{'22207878',{'34707836',"example glossary",
                            {'6257036',"S",
                                       {'131402670',{'49933946',"SGML","SGML",
                                                                "Standard Generalized Markup Language","SGML",
                                                                "ISO 8879:1986",
                                                                {'111785629',"A meta-markup language, used to create markup languages such as DocBook.",
                                                                             ["GML","XML"]},
                                                                "markup"}}}}}

Looking at content of record definition file :

$ cat /tmp/records.hrl
-record('111785629', {para  = []  :: list(), 'GlossSeeAlso'  = []  :: list()}).
-record('49933946', {'ID'  = []  :: list(), 'SortAs'  = []  :: list(), 'GlossTerm'  = []  :: list(), 'Acronym'  = []  :: list(), 'Abbrev'  = []  :: list(), 'GlossDef'  = '111785629':new()  :: '111785629':'111785629'(), 'GlossSee'  = []  :: list()}).
-record('131402670', {'GlossEntry'  = '49933946':new()  :: '49933946':'49933946'()}).
-record('6257036', {title  = []  :: list(), 'GlossList'  = '131402670':new()  :: '131402670':'131402670'()}).
-record('34707836', {title  = []  :: list(), 'GlossDiv'  = '6257036':new()  :: '6257036':'6257036'()}).
-record('22207878', {glossary  = '34707836':new()  :: '34707836':'34707836'()}).

Now edit a sample module :

-module(test).

-include("/tmp/records.hrl").

-export([test/1]).

test(File) -> A = jason:decode_file(File, [{mode, record}]),
              io:format("~p~n", [A#'22207878'.glossary#'34707836'.'GlossDiv'#'6257036'.'GlossList'#'131402670'.'GlossEntry'#'49933946'.'GlossTerm']).

Compile and test module :

1> c(test).
{ok,test}
2> test:test("git/jason/priv/ex1.json").
"Standard Generalized Markup Language"
ok

Renaming record definition

Dynamic names of argonaut modules are hard to remember. If JSON structure is stable, we can rename records for simpler names and tell jason to use those. But be aware that jason won't create argonaut helper modules in such case.

First let's rename records in record definition (using dummy a,b,c,etc.. names for this example, but meaningful names can be used obviously). Note that argonaut opaque types need to be removed from declaration, as no argonaut modules will be created. Jason will only trust your record declaration.

-record('f', {para  = []  :: list(), 'GlossSeeAlso'  = []  :: list()}).
-record('e', {'ID'  = []  :: list(), 'SortAs'  = []  :: list(), 'GlossTerm'  = []  :: list(), 'Acronym'  = []  :: list(), 'Abbrev'  = []  :: list(), 'GlossDef'  = #'f'{}, 'GlossSee'  = []  :: list()}).
-record('d', {'GlossEntry'  = #'e'{}}).
-record('c', {title  = []  :: list(), 'GlossList'  = #'d'{}}).
-record('b', {title  = []  :: list(), 'GlossDiv'  = #'c'{}}).
-record('a', {glossary  = #'b'{}}).

Using module record definitions

Then let's modify our test module in order to use our private record definition from module itself :

-module(test).

-include("/tmp/records.hrl").

-export([test/1]).

test(File) -> 
              A = jason:decode_file(File, [{mode, record}, {records, [?MODULE ]}]),
              io:format("~p~n", [A#'a'.glossary#'b'.'GlossDiv'#'c'.'GlossList'#'d'.'GlossEntry'#'e'.'GlossTerm']).

NOTE: As we ask jason to extract records declaration from test module abstract code, option debug_info is given to compiler hereafter. Please ensure it will be the case in your usual compilation process (normally yes with usual build tools).

1> c(test,[debug_info]).
{ok,test}
2> test:test("priv/ex1.json").
"Standard Generalized Markup Language"
ok

Using argonaut modules with understandable names

Since version 1.2.0, jason can create argonaut modules from your own record names, called aliases. Simply replace all hashes names by your own name aliases in .hrl file generated by jason.

-record('F', {para  = []  :: list(), 'GlossSeeAlso'  = []  :: list()}).
-record('E', {'ID'  = []  :: list(), 'SortAs'  = []  :: list(), 'GlossTerm'  = []  :: list(), 'Acronym'  = []  :: list(), 'Abbrev'  = []  :: list(), 'GlossDef'  = 'f':new()  :: 'F':'F'(), 'GlossSee'  = []  :: list()}).
-record('D', {'GlossEntry'  = 'E':new()  :: 'E':'E'()}).
-record('C', {title  = []  :: list(), 'GlossList'  = 'D':new()  :: 'D':'D'()}).
-record('B', {title  = []  :: list(), 'GlossDiv'  = 'C':new()  :: 'C':'C'()}).
-record('A', {glossary  = 'B':new()  :: 'B':'B'()}).

Note : as c module already exists in OTP, records are called with capital letter to avoid any module name clash in this example. It is up to you to check this if lowercase module names are used.

Now edit your test module to change thing accordingly :

-module(test).

-include("/tmp/records.hrl").

-export([test/1]).

test(File) -> 
              A = jason:decode_file(File, [{mode, record}, {aliases, [?MODULE ]}]),
              io:format("~p~n", [A#'A'.glossary#'B'.'GlossDiv'#'C'.'GlossList'#'D'.'GlossEntry'#'E'.'GlossTerm']).

Let's test this module :

1>  c(test,[debug_info]).
{ok,test}
2>  test:test("priv/ex1.json").
"Standard Generalized Markup Language"
ok
3>'A':def().
"-record('A', {glossary  = 'B':new()  :: 'B':'B'()})."

Now argonaut modules are available with handy names for all your records. This can be checked by looking at your current process jason_adhoc registry.

4> get(jason_adhoc).
['F','E','D','C','B','A']

We can decide (or not) to dump them on disk. This to avoid any module creation time overhead in future.

Dump modules on disk

Either by asking to dump all modules (recommended) with jason:dump/1 or individual modules with jason:dump/2. Modules have to be dumped in a directory in path known by emulator for automatic loading, as any other Erlang module.

Important : Only argonaut modules from aliases options can be dumped, all other dynamic argonaut modules cannot.

Note : if jason_adhoc is empty (the case if jason:dump/x is called from another Pid than the one which creates modules), jason will search all argonaut modules in the whole system. If not, jason will check that modules listed in jason_adhoc are really argonaut ones.

5> jason:dump("/tmp/dump/"). % Only for testing as /tmp/ is not a safe directory (emptied at reboot)
ok

Note : Modules will be moved in usual OTP directory structure under root directory given.

$> tree /tmp/dump
/tmp/dump
├── A
│   └── ebin
│       └── A.beam
├── B
│   └── ebin
│       └── B.beam
├── C
│   └── ebin
│       └── C.beam
├── D
│   └── ebin
│       └── D.beam
├── E
│   └── ebin
│       └── E.beam
└── F
    └── ebin
        └── F.beam

JSON to records translation when source is not stable

Let use a JSON source giving temperature of some cities, coming from third party, from an HTTP API for instance :

weather.json

[
 { "city" : "Paris" ,
   "temperature" : 15.6,
   "unit" : "celcius"
 },
 { "city" : "Berlin" ,
   "temperature" : 13.6 ,
   "unit" : "celcius"
 },
 { "city" : "London" ,
   "temperature" : 12.3 ,
   "unit" : "celcius"
 }
]

This API is announced to be possibly changing, by adding some other informations. Using records in Erlang would be a problem if those ones have to be declared in code. Use of jason and some coding precaution solve this problem :

-module(test).

-export([test/1]).

test(File) -> A = jason:decode_file(File, [{mode, record}]),
              lists:foreach(fun(R) -> Argonaut = element(1, R), 
                                      io:format("~w ~s~n", [Argonaut:temperature(R), 
                                                            Argonaut:unit(R)]) 
                            end, A).

But what if JSON change, by adding a new field in records ? Nothing.

weather1.json

[
 { "city" : "Paris" ,
   "temperature" : 15.6,
   "unit" : "celcius",
   "EEC" : true 
 },
 { "city" : "Berlin" ,
   "temperature" : 13.6 ,
   "unit" : "celcius",
   "EEC" : true 
 },
 { "city" : "London" ,
   "temperature" : 12.3 ,
   "unit" : "celcius",
   "EEC" : false
 }
]
1> c(test).
{ok,test}
2> test:test("/tmp/weather.json").
15.6 celcius
13.6 celcius
12.3 celcius
ok
3> test:test("/tmp/weather1.json").
15.6 celcius
13.6 celcius
12.3 celcius
ok

Jason create an argonaut module for each new JSON structure, and this argonaut module can be used dynamically in the code. Obviously, other precautions can be done by checking if temperature/1 function is existing in argonaut module. If not the case, mean that JSON source is invalid or changed too much...