Rick Strahl's FoxPro and Web Connection Web Log
White Papers | Products | Message Board | News |

Calling JSON REST Services with FoxPro and wwJsonServiceClient


Thursday, May 26, 2016, 11:54:21 PM

A few days ago I released version 6.0 of the West Wind Client Tools which includes a little gem of a class called wwJsonServiceClient that makes it super easy to call JSON REST services. This class is part of the West Wind Client Tools as well as West Wind Web Connection and it makes it a snap to call JSON services to receive and send data.

Calling REST Services

REST services are becoming more and more popular and are starting to crowd out SOAP based services as the vehicle to share raw data over the Internet. Where SOAP is a very heavy XML based protocol that requires a very strict format, REST generally uses much simpler structures to push data over the wire. Data is usually encoded as JSON rather than XML. JSON is essentially a way to express JavaScript values, objects and arrays as a literal string value, which is easy to create and parse using standard libraries. Like XML, JSON is a serialization format, but unlike XML it skips all the formality and provides just the data.

If you're retrieving data from online social services or 'new Web' companies, you're going to encounter REST and JSON APIs generally. The benefit of REST JSON services is that generally they are much simpler than SOAP counterparts as you don't have worry about XML data mapping descreprancies. You don't need any special software to access a REST service - there's no SOAP client that has to parse a SOAP message and call. You simply have a documented endpoint and a set structure of data to send in (if any) and a set structure of data that is returned. All you need to access a REST service is an HTTP client (like wwHttp) and if the data is in JSON format a JSON serializer (like wwJsonSerializer).

On the server side things are also easier because again there's not additional contract that needs to be created. To create a REST service you build plain HTTP endpoints and return a JSON value or object. If you're using FoxPro in Web Connection the wwRestProcess class makes it easy to create FoxPro based REST services by simply creating classes that take a single input parameter and return a single result value. The wwRestProcess class handles all the logistics of publishing that data.

What is wwJsonServiceClient?

You can think of wwJsonServiceClient as a simple way to call a REST service. At it's simplest it's a high level wrapper class that wraps the wwHttp and wwJsonSerializer to call JSON services via HTTP and handles the JSON serialization and deserialization.

In a nutshell it reduces calling a REST service to a single line of code.

Making a GET Request

Let's try it out. The following calls a sample site to retrieve a list of Music Albums from an online AlbumViewer sample I created for last year's Web Connection Training.

This first request is a simple GET command that retrieves the list of albums like so:

do wwhttp
do wwJsonSerializer

loProxy = CREATEOBJECT("wwJsonServiceClient")

*** Make the service call
loAlbums = loProxy.CallService("http://albumviewerswf.west-wind.com/api/albums",
                               "","GET")
? loProxy.cErrorMsg

lnCount = loAlbums.Count

* ? loAlbums.Item(1).Title

FOR EACH loAlbum in loAlbums 
   ? loAlbum.Title  + ;
     " by " + loAlbum.Artist.ArtistName + ;
     " (" +  TRANSFORM(loAlbum.Tracks.Count) + " tracks)"
   FOR EACH loTrack IN loAlbum.Tracks
		? "  " + loTrack.SongName
   ENDFOR
ENDFOR

The key is this line:

loAlbums = loProxy.CallService("http://albumviewerswf.west-wind.com/api/albums","","GET")

which makes an HTTP GET request to the server with not data to send to the server (empty second parameter). The last two parameters are actually not required - if no data is passed GET is assumed for the HTTP verb. You've just simplified a service call to single line of code.

POSTing data to a server

Along the same lines you can also post data to a server by passing a value, object or array to the server as the second parameter and changing the HTTP verb.

loArtist = CREATEOBJECT("Empty")
ADDPROPERTY(loArtist,"Id","2")
ADDPROPERTY(loArtist,"ArtistName","Accept")
ADDPROPERTY(loArtist,"Description","Old school German Metal band")
ADDPROPERTY(loArtist,"ImageUrl","http://cps-static.rovicorp.com/3/JPG_400/MI0001/389/MI0001389322.jpg?partner=allrovi.com")
ADDPROPERTY(loArtist,"AmazonUrl","")

loProxy = CREATEOBJECT("wwJsonServiceClient")
loArtist = loProxy.CallService("http://albumviewerswf.west-wind.com/api/artist",loArtist,"PUT")

IF  loProxy.lError
   ? loProxy.cErrorMsg
   RETURN
ENDIF

? loArtist.Albums.Count

Now in this case the call fails because you actually have to log in first, but if you hook up an HTTP proxy like Fiddler you you'll see the following was sent to the server:

PUT http://albumviewerswf.west-wind.com/api/artist HTTP/1.1
Content-Type: application/json
User-Agent: West Wind Internet Protocols 6
Host: albumviewerswf.west-wind.com
Content-Length: 194
Pragma: no-cache
Cookie: _ga=GA1.2.1516538738.1449808058; albv=C03E01F1F67F15336

{ 
 "amazonurl":"",
 "artistname":"Accept",
 "description":"Old school German Metal band",
 "id":"2",
 "imageurl":"http://cps-static.rovicorp.com/3/JPG_400/MI0001/389/MI0001389322.jpg?partner=allrovi.com"
}

So you can see that the service client is generating an HTTP request with a JSON payload from your method call.

Dealing with FoxPro Case Limitations

Now it turns out that the service actually expects all the property names to be proper case. FoxPro's property retrieval API unfortunately does not natively support preserving case (except if you use MemberData which is a pain in the ass) and so the serializer by default turns all property names into lower case.

If you need to communicate with a service that requires either proper or camel case (which is common for JSON services) you can override property names explicitly.

loProxy = CREATEOBJECT("wwJsonServiceClient")

*** Create a custom serializer that overrides property names
loSer = CREATEOBJECT("wwJsonSerializer")
loSer.PropertyNameOverrides = "Id,ArtistName,Description,ImageUrl,AmazonUrl"
loProxy.CreateSerializer(loSer)

loArtist = loProxy.CallService("http://albumviewerswf.west-wind.com/api/artist",loArtist,"PUT")

IF  loProxy.lError
   ? loProxy.cErrorMsg
   RETURN
ENDIF

? loArtist.Albums.Count

Now the data is sent with proper names:

{ 
 "AmazonUrl":"",
 "Artistname":"Accept",
 "Description":"Old school German Metal band",
 "Id":"2",
 "ImageUrl":"http://cps-static.rovicorp.com/3/JPG_400/MI0001/389/MI0001389322.jpg?partner=allrovi.com"
}

These overrides are applied globally to all properties, child properties and array items so this addresses the FoxPro type name limitations.

HTTP Verbs

The above example used a PUT command to update an existing record. POST is typically used to add a new record, there's also DELETE and HEAD and a few other verbs. The verbs are usually determined by the service interface which should be provided by the documentation for the service.

Verbs can be used to differentiate operations on the same URL. For example, in the Item Service above I can use the Artist endpoint with GET and an ID to retrieve an artist. With POST a new artist is added, PUT updates an artist and DELETE removes an artist all from the same http://albumviewerswf.west-wind.com/api/artist Url. This is a common pattern and this approach lets you work with that.

Make it Better

Ok, so the above works, but you really don't want to scatter code like this into your application logic. It's a much better idea to abstract the service implementation into its own class with methods for each of the operations you want to perform.

Instead I recommend that when you access a service, you create a class that inherits from wwJsonServiceClient and then implement methods that make the service call. This can simply error handling and puts all the code related to the service - including any possible pre and post processing you might do for each call - into a single class.

So if we refactor our two service functions we might end up with a class like this:

******************************************************
DEFINE CLASS AlbumViewerService as wwJsonServiceClient
******************************************************

************************************************************************
*  Init
****************************************
FUNCTION Init()

DODEFAULT()

*** Ensure a serializer exists
this.CreateSerializer()

ENDFUNC

************************************************************************
*  GetAlbums
****************************************
FUNCTION GetAlbums()
LOCAL loAlbums

loAlbums = THIS.CallService("http://albumviewerswf.west-wind.com/api/albums","","GET")

*** Error message is already set
IF THIS.lError
   RETURN null
ENDIF   

RETURN loAlbums
ENDFUNC


************************************************************************
*  UpdateArtist
****************************************
FUNCTION UpdateArtist(loArtist)
LOCAL loArtist

this.oSerializer.PropertyNameOverrides = "Id,ArtistName,Description,ImageUrl,AmazonUrl"

loArtist = THIS.CallService("http://albumviewerswf.west-wind.com/api/artist",loArtist,"PUT")

*** Reset overridden properties
this.oSerializer.PropertyNameOverrides = ""

IF THIS.lError
   RETURN null
ENDIF   

RETURN loArtist
ENDFUNC


ENDDEFINE

By doing this you're abstracting all the logic required to deal with the service in this class, so the application doesn't need to see this low level interaction.

To use the service and call both methods the front end code you might use inside of your application looks like this:

*** Create our CUSTOM service
loProxy = CREATEOBJECT("AlbumViewerService")

*** call the wrapper method
loAlbums = loProxy.GetAlbums()

lnCount = loAlbums.Count
* Do something with the data


loArtist = CREATEOBJECT("Empty")
ADDPROPERTY(loArtist,"Id","2")
ADDPROPERTY(loArtist,"ArtistName","Accept")
ADDPROPERTY(loArtist,"Description","Old school German Metal band")
ADDPROPERTY(loArtist,"ImageUrl","http://cps-static.rovicorp.com/3/JPG_400/MI0001/389/MI0001389322.jpg?partner=allrovi.com")
ADDPROPERTY(loArtist,"AmazonUrl","")

loAlbum = loProxy.UpdateArtist(loArtist)
IF  loProxy.lError
   ? loProxy.cErrorMsg
   RETURN
ENDIF

? loArtist.Albums.Count

RETURN 

This is much cleaner as you are calling simple methods that describe what's happening. If something goes wrong, you have one place to go to look for the failures and the service becomes now reusable.

Summary

REST services are becoming much more common and although they are easy to call if you have an HTTP client and JSON serializer, wwJsonSerializer offers a more simpler, more abstracted and consistent way of doing it for you. Errors are caught and passed back to you and it reduces the service call to essentially a single line of code potentially with some extra configuration if you need to post data to the server that requires proper casing.

Give wwJsonSerializer a try. It's available in West Wind Client Tools 6.0 and West Wind Web Connection, but the Web Connection version is not quite up to date and is missing the CreateSerializer() method. This will be addressed in the forthcoming v6.05 release due out shortly.

Posted in: FoxPro    Web Connection

Feedback for this Weblog Entry


re: Calling JSON REST Services with FoxPro and wwJsonServiceClient


Hi Rick,

Nice addition in west wind client tools. I have following questions.

1) In VFp, table structure can be passed with cursortoXml. Is it possible with Json services?

2) How memo field data is passed with Json services ?

TIA
Naeem

re: Calling JSON REST Services with FoxPro and wwJsonServiceClient


1) Yes you can return cursor data by using "cursor:TQuery" as a string to specify the cursor in a property. The cursor is turned into an array and sent as JSON. Can be the top level result or set on a property assuming the cursor is open. Cursor serialization is one way - you can't directly deserialize an array into a cursor. You can however use CollectionToCursor() to dump the result object back into a cursor/table as long as the table exists.

2) Memo fields are just text and it just works. Nothing special about memo fields. Binary data is converted base64 and sent.

re: Calling JSON REST Services with FoxPro and wwJsonServiceClient



Jon
Thank you Rick--great stuff as always. And a cool plug for a great band in your example (Accept)!
 



© Rick Strahl, West Wind Technologies, 2003 - 2017