Consuming JSON from an API
Source code for the project is available for download, unfortunately as we're discussing server-side Prolog we can't safely make this post interactive.
We begin by including a couple of useful library modules, one to open the URL and another to read the JSON document out into a dictionary.
:- use_module(library(http/http_open),
[ http_open/3
]
).
:- use_module(library(http/json),
[ json_read_dict/2
, json_read_dict/3
]
).
We're importing both arities of json_read_dict
so we can discuss using it both with and without options.
The last thing we need is an endpoint URL to request data from. We'll put this into a fact so as to mimic a more realistic application.
location_url('https://jsonplaceholder.typicode.com/users/').
This endpoint is a free service provided to help developers, many thanks to their sponsors.
Getting the data
We're going to open a stream asking the endpoint for JSON. We'll then read that JSON into a SWI-Prolog dict from the open stream. Finally we'll close the stream. Just in case something goes awry, we'll wrap these three tasks in setup_call_cleanup/3
, which will ensure the stream is closed via cleaning up.
get_json(Dict) :-
location_url(URL),
setup_call_cleanup(
http_open(URL, StreamIn, [request_header('Accept'='application/json')]),
json_read_dict(StreamIn, Dict),
close(StreamIn)
).
There are a few ways to specify headers in the options for http_open/3
, this is one of the annoying little quirks. However if you use this way it'll work!
JSON Read Dict Options
There are two common options that you may find useful for json_read_dict/3
. The first lets you set the tag of the dict, which can be very useful when organizing multiple dicts. Consider the following:
?- list_to_set([_{a: 1}, _{a: 1}], Set).
Set = [_{a: 1}, _{a: 1}].
false.
?- list_to_set([t{a: 1}, t{a: 1}], Set).
Set = [t{a: 1}].
false.
Only with the tag set can dicts be proved to unify.
The other useful option lets you specify how you'd like the values, by default they are strings but given the wide usage of atoms you may wish to change this. So to adapt the previous predicate for getting data, we could add these options:
get_json(Dict) :-
location_url(URL),
setup_call_cleanup(
http_open(URL, StreamIn, [request_header('Accept'='application/json')]),
json_read_dict(StreamIn, Dict, [tag(json), value_string_as(atom)]),
close(StreamIn)
).
Run It!
Load it up, run it, bookmark this page and the next time you need to grab some data from a JSON API you'll be smiling! (Unless the server doesn't set the encoding type... still haven't got a work around for that one). Just for fun, here's something to apply the endpoint in the example code:
describe :-
format("Users:~n"),
get_json(D),
describe(D).
describe([]).
describe([H|T]) :-
format(" ~w. ~w~n", [H.get(name), H.get(email)]),
describe(T).