Using SAS DS2 to parse JSON

Update 02Dec2016: Beginning with SAS 9.4 Maintenance 4, there is now a JSON libname engine. Read this new article to learn more -- you might prefer it to using DS2 for this task!

Thanks to the proliferation of cloud services and REST-based APIs, SAS users have been making use of PROC HTTP calls (to query these web services) and some creative DATA step or PROC GROOVY code to process the JSON results. Such methods get the job done (JSON is simply text, after all), but they aren't as robust as an official JSON parser. JSON is simple: it's a series of name-value pairs that represent an object in JavaScript. But these pairs can be nested within one another, so in order to parse the result you need to know about the object structure. A parser helps with the process, but you still need to know the semantics of any JSON response.

SAS 9.4 introduced PROC JSON, which allows you to create JSON output from a data set. But it wasn't until SAS 9.4 Maintenance 3 that we have a built-in method to parse JSON content. This method was added as a DS2 package: the JSON package.

I created an example of the method working -- using an API that powers our SAS Support Communities! The example queries communities.sas.com for the most recent posts to the SAS Programming category. Here's a small excerpt of the JSON response.

"post_time": "2015-09-28T16:29:05+00:00",
"views": {
"count": 1
},
"subject": "Re: How to code for the consecutive values",
"author": {
"href": "\/users\/id\/13884",
"login": "ballardw"

Notice that some items, such as post_time, are simple one-level values. But other items, such as views or author, require a deeper dive to retrieve the value of interest ("count" for views, and "login" for author). The DS2 JSON parser can help you to navigate to those values without you needing to know how many braces or colons or commas are in your way.

Here is an example of the result: a series plot from PROC SGPLOT and a one-way frequency analysis from PROC FREQ. The program also produces a detailed listing of the messages, the topic content, and the datetime stamp.

This is my first real DS2 program, so I'm open to feedback. I already know of a couple of improvements I should make, but I want to share it now as I think it's good enough to help others who are looking to do something similar.

The program requires SAS 9.4 Maintenance 3. All of the code runs using just Base SAS procedures.

/* DS2 program that uses a REST-based API */
/* Uses http package for API calls */
/* and the JSON package (new in 9.4m3) */
/* to parse the result. */
proc ds2;
data messages (overwrite=yes);
/* Global package references */
dcl package json j();

/* Keeping these variables for output */
dcl double post_date having format datetime20.;
dcl int views;
dcl nvarchar(128) subject author board;

/* these are temp variables */
dcl varchar(65534) character set utf8 response;
dcl int rc;
drop response rc;

method parseMessages();
dcl int tokenType parseFlags;
dcl nvarchar(128) token;
rc=0;
* iterate over all message entries;
do while (rc=0);
j.getNextToken( rc, token, tokenType, parseFlags);

* subject line;
if (token eq 'subject') then
do;
j.getNextToken( rc, token, tokenType, parseFlags);
subject=token;
end;

* board URL, nested in an href label;
if (token eq 'board') then
do;
do while (token ne 'href');
j.getNextToken( rc, token, tokenType, parseFlags );
end;
j.getNextToken( rc, token, tokenType, parseFlags );
board=token;
end;

* number of views (int), nested in a count label ;
if (token eq 'views') then
do;
do while (token ne 'count');
j.getNextToken( rc, token, tokenType, parseFlags );
end;
j.getNextToken( rc, token, tokenType, parseFlags );
views=inputn(token,'5.');
end;

* date-time of message (input/convert to SAS date) ;
* format from API: 2015-09-28T10:16:01+00:00 ;
if (token eq 'post_time') then
do;
j.getNextToken( rc, token, tokenType, parseFlags );
post_date=inputn(token,'anydtdtm26.');
end;

* user name of author, nested in a login label;
if (token eq 'author') then
do;
do while (token ne 'login');
j.getNextToken( rc, token, tokenType, parseFlags );
end;
* get the author login (username) value;
j.getNextToken( rc, token, tokenType, parseFlags );
author=token;
output;
end;
end;
return;
end;

method init();
dcl package http webQuery();
dcl int rc tokenType parseFlags;
dcl nvarchar(128) token;
dcl integer i rc;

/* create a GET call to the API */
/* 'sas_programming' covers all SAS programming topics from communities */
webQuery.createGetMethod(
'http://communities.sas.com/kntur85557/' ||
'restapi/vc/categories/id/sas_programming/posts/recent' ||
'?restapi.response_format=json' ||
'&restapi.response_style=-types,-null&page_size=100');
/* execute the GET */
webQuery.executeMethod();
/* retrieve the response body as a string */
webQuery.getResponseBodyAsString(response, rc);
rc = j.createParser( response );
do while (rc = 0);
j.getNextToken( rc, token, tokenType, parseFlags);
if (token = 'message') then
parseMessages();
end;
end;

method term();
rc = j.destroyParser();
end;

enddata;
run;
quit;

/* Add some basic reporting */
proc freq data=messages noprint;
format post_date datetime11.;
table post_date / out=message_times;
run;

ods graphics / width=2000 height=600;
title '100 recent message contributions in SAS Programming';
title2 'Time in GMT';
proc sgplot data=message_times;
series x=post_date y=count;
xaxis minor label='Time created';
yaxis label='Messages' grid;
run;

title 'Board frequency for recent 100 messages';
proc freq data=messages order=freq;
table board;
run;

title 'Detailed listing of messages';
proc print data=messages;
run;

title;

I also shared this program on the SAS Support Communities as a discussion topic. If you want to contribute to the effort, please leave me a reply with your suggestions and improvements!