Monday, November 30, 2015

Accessing a JSON webservice using libsoup and JSON-GLib

A lot of web services use the JSON format. If you are working a GLib based project and need to access a service like this there are two great libraries to help you - libsoup and JSON-Glib.

For my example, I'm going to grab some review data from Ubuntu (API) which looks something like this:

[
    {
        "ratings_total": 229,
        "ratings_average": "3.84",
        "app_name": "",
        "package_name": "simple-scan",
        "histogram": "[35, 13, 22, 42, 117]"
    },
    {
        "ratings_total": 546,
        "ratings_average": "4.66",
        "app_name": "",
        "package_name": "audacity",
        "histogram": "[17, 7, 17, 63, 442]"
    },

    ...
]

The data is a single array of objects that contain the statistics for each package. For this example I'll print out the number of ratings for each package by getting the package_name and ratings_total members from each object.

Firstly, I need to download the data. The data is retrieved using a HTTP GET request; in libsoup you can do this with:

    SoupSession *session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, "test-json", NULL);
    SoupMessage *message = soup_message_new (SOUP_METHOD_GET, "https://reviews.ubuntu.com/reviews/api/1.0/review-stats/any/any");

    soup_session_send_message (session, message);

Now I have the server text in message->response_body->data but it needs to be decoded. JSON-GLib can parse it with:

    JsonParser *parser = json_parser_new ();
    json_parser_load_from_data (parser, message->response_body->data, -1, NULL);
    JsonNode *root = json_parser_get_root (parser);


Now I have an in-memory tree of the JSON data which can be traversed. After checking the root node is an array as expected I'll iterate over each object:

    g_assert (JSON_NODE_HOLDS_ARRAY (root));
    array = json_node_get_array (root);
    for (i = 0; i < json_array_get_length (array); i++)
    {
        JsonNode *node = json_array_get_element (array, i);

        /* do stuff... */
    }

For each object, I extract the required data:

        g_assert (JSON_NODE_HOLDS_OBJECT (node));
        JsonObject *object = json_node_get_object (node);
        const gchar *package_name = json_object_get_string_member (object, "package_name");
        gint64 ratings_total = json_object_get_int_member (object, "ratings_total");
        if (package_name)
            g_print ("%s: %" G_GUINT64_FORMAT "\n", package_name,

 
Combined into a program, I can print out the number of reviews for each package:

simple-scan: 229
audacity: 546
...


The full program:

// gcc -g -Wall example-json.c -o example-json `pkg-config --cflags --libs libsoup-2.4 json-glib-1.0`

#include
#include

int main (int argc, char **argv)
{
    SoupSession *session;
    SoupMessage *message;
    guint status_code;
    JsonParser *parser;
    gboolean result;
    JsonNode *root;
    JsonArray *array;
    gint i;

    /* Get the data using a HTTP GET */
    session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, "test-json", NULL);
    message = soup_message_new (SOUP_METHOD_GET, "https://reviews.ubuntu.com/reviews/api/1.0/review-stats/any/any");
    g_assert (message != NULL);
    status_code = soup_session_send_message (session, message);
    g_assert (status_code == SOUP_STATUS_OK);

    /* Parse the data in JSON format */
    parser = json_parser_new ();
    result = json_parser_load_from_data (parser, message->response_body->data, -1, NULL);
    g_assert (result);

    /* The data should contain an array of JSON objects */
    root = json_parser_get_root (parser);
    g_assert (JSON_NODE_HOLDS_ARRAY (root));
    array = json_node_get_array (root);
    for (i = 0; i < json_array_get_length (array); i++)
    {
        JsonNode *node;
        JsonObject *object;
        const gchar *package_name;
        gint64 ratings_total;

        /* Get the nth object, skipping unexpected elements */
        node = json_array_get_element (array, i);
        if (!JSON_NODE_HOLDS_OBJECT (node))
            continue;

        /* Get the package name and number of ratings from the object - skip if has no name */
        object = json_node_get_object (node);
        package_name = json_object_get_string_member (object, "package_name");
        ratings_total = json_object_get_int_member (object, "ratings_total");
        if (package_name)
            g_print ("%s: %" G_GINT64_FORMAT "\n", package_name, ratings_total);
    }

    /* Clean up */
    g_object_unref (session); 
    g_object_unref (message);
    g_object_unref (parser);

    return 0;
}


And to show you can do the same thing with GIR bindings, here's the same in Vala:

// valac example-json.vala --pkg soup-2.4 --pkg json-glib-1.0

public int main (string[] args)
{
    /* Get the data using a HTTP GET */
    var session = new Soup.Session.with_options (Soup.SESSION_USER_AGENT, "test-json");
    var message = new Soup.Message ("GET", "https://reviews.ubuntu.com/reviews/api/1.0/review-stats/any/any");
    assert (message != null);
    var status_code = session.send_message (message);
    assert (status_code == Soup.Status.OK);

    /* Parse the data in JSON format */
    var parser = new Json.Parser ();
    try
    {
        parser.load_from_data ((string) message.response_body.data);
    }
    catch (Error e)
    {
    }

    /* The data should contain an array of JSON objects */
    var root = parser.get_root ();
    assert (root.get_node_type () == Json.NodeType.ARRAY);
    var array = root.get_array ();
    for (var i = 0; i
    {
        /* Get the nth object, skipping unexpected elements */
        var node = array.get_element (i);
        if (node.get_node_type () != Json.NodeType.OBJECT)
            continue;

        /* Get the package name and number of ratings from the object - skip if has no name */
        var object = node.get_object ();
        var package_name = object.get_string_member ("package_name");
        var ratings_total = object.get_int_member ("ratings_total");
        if (package_name != null)
            stdout.printf ("%s: %" + int64.FORMAT + "\n", package_name, ratings_total);
    }

    return 0;
}

and Python:

#!/usr/bin/python

from gi.repository import Soup
from gi.repository import Json

# Get the data using a HTTP GET
session = Soup.Session.new ()
session.set_property (Soup.SESSION_USER_AGENT, "test-json")
message = Soup.Message.new ("GET", "https://reviews.ubuntu.com/reviews/api/1.0/review-stats/any/any")
assert (message != None)
status_code = session.send_message (message)
assert (status_code == Soup.Status.OK)

# Parse the data in JSON format
parser = Json.Parser ()
parser.load_from_data (message.response_body.data, -1)

# The data should contain an array of JSON objects
root = parser.get_root ()
assert (root.get_node_type () == Json.NodeType.ARRAY)
array = root.get_array ()
for i in xrange (array.get_length ()):
    # Get the nth object, skipping unexpected elements
    node = array.get_element (i)
    if node.get_node_type () != Json.NodeType.OBJECT:
        continue

    # Get the package name and number of ratings from the object - skip if has no name
    object = node.get_object ()
    package_name = object.get_string_member ("package_name")
    ratings_total = object.get_int_member ("ratings_total")
    if package_name != None:
        print ("%s: %d" % (package_name, ratings_total))