JSON Serialization Formats
The wwJSONSerializer class serializes FoxPro values in JSON formatted strings. This topic demonstrates how to create output for various types and what the output looks like:
String
Let's start with a simple string serialized into JSON:
DO wwJsonSerializer && Load Libs
loSerializer = CREATEOBJECT("wwJsonSerializer")
lcJSON = loSerializer.Serialize("Hello Rick")
returns: "Hello Rick"
(including the quotes)
Dates
Dates are serialized and parsed in ISO format which looks like this:
loSerializer = CREATEOBJECT("wwJsonSerializer")
lcJSON = loSerializer.Serialize(DateTime())
returns: "2011-01-08T04:34:56Z"
There's also an option flag that supports Microsoft's old MSAJAX style serialization format.
Numbers and Booleans
All other simple types are created using standard JavaScript literals for types. Numbers are created as simple number values (decimal style). Booleans are stored as true and false.
Binary Values
Binary values are serialized as Base64 string which is standard for JSON data.
However, when deserializing binary data from JSON the value is returned as a Base64 String value. To get the binary value, you have to explicitly convert the value from string to binary using STRCONV(lvData, 14)
.
loObj = CREATEOBJECT("EMPTY")
ADDPROPERTY(loObj,"Name", "Rick")
ADDPROPERTY(loObj,"Data", CAST( "ASDAS" as BLOB))
loSer = CREATEOBJECT("wwJsonSerializer")
lcJson = loSer.Serialize(loObj)
? lcJson && {"data": "QVNEQVM=","name": "Rick"}
loObj2 = loSer.Deserialize(lcJson)
? loObj2.Name && regular string
? loObj2.Data && base64 string: QVNEQVM=
? STRCONV(loObj2.Data,14) && ASDAS
Objects
You can also serialize objects. Here's an example of a hierarchical object:
loCustomer = CREATEOBJECT("EMPTY")
ADDPROPERTY(loCustomer,"Company","West Wind Technologies")
ADDPROPERTY(loCustomer,"Name","Rick Strahl")
ADDPROPERTY(loCustomer,"Entered",DATETIME())
loAddress = CREATEOBJECT("EMPTY")
ADDPROPERTY(loAddress,"Street","32 Kaiea")
ADDPROPERTY(loAddress,"City","Paia")
ADDPROPERTY(loAddress,"State","HI")
ADDPROPERTY(loAddress,"Zip","96779")
ADDPROPERTY(loAddress,"Occupants",1)
ADDPROPERTY(loCustomer,"Address",loAddress)
loSerializer = CREATEOBJECT("wwJsonSerializer")
loSerializer.FormattedOutput = .T. && prettify the JSON
lcJSON = loSerializer.Serialize(loCustomer)
which yields:
{
"address": {
"city": "Paia",
"occupants": 1,
"state": "HI",
"street": "32 Kaiea",
"zip": "96779"
},
"company": "West Wind Technologies",
"entered": "2015-10-27T22:41:17Z",
"name": "Rick Strahl"
}
Formatted Output
Note that this is formatted output which is created when the
.FormattedOutput
property istrue
. By default the serializer produces unformatted output, which is a single line of text.
All properties are created inside of JavaScript object notation () and an inner object is created for the address. As you can see JSON is self-describing in that there's no schema required - the format lets JavaScript know what type you're dealing with.
Proper Case and Customized Property Names
By default all object properties from FoxPro are rendered in lower case due to FoxPro's inability to return properly cased generic runtime type information.
You can optionally override individual property names generated using the .PropertyNameOverrides property.
loSerializer = CREATEOBJECT("wwJsonSerializer")
loSerializer.FormattedOutput = .T.
loSerializer.PropertyNameOverrides = ;
"Address,City,Occupants,State,Street,Zip,Company,Entered,Name"
lcJSON = loSerializer.Serialize(loCustomer)
Simply create a comma delimited string that includes all the property names you want to override, using the proper case you'd like to see. Note that you only need to provide those properties that need to be overwritten.
Internally this replaces all the lower case properties with the explicitly provided properties:
{
"Address": {
"City": "Paia",
"Occupants": 1,
"State": "HI",
"Street": "32 Kaiea",
"Zip": "96779"
},
"Company": "West Wind Technologies",
"Entered": "2015-10-27T22:43:02Z",
"Name": "Rick Strahl"
}
Keep in mind that most JavaScript objects are expected to be camelCased - meaning lower case first word with upper cased subsequent words (ie. camelCase, birthDate, firstName, propertyNameOverrides etc.) so typically you need to apply name overrides only only longer, multi-part words.
Mapping Properties to Custom Property Names
Sometimes transforming the case of a property isn't enough and you need to produce a property (or Map name) that can't be created via a FoxPro property name. For example, properties that start with a number, contain spaces or extended characters or other any number of other naming scenarios that FoxPro properties doesn't support.
You can use .MapPropertyName() to transform any property name in a JSON string to any other name. The example shows how to create property/map names with spaces:
loSer = CREATEOBJECT("wwJsonSerializer")
loObj = CREATEOBJECT("EMPTY")
ADDPROPERTY(loObj,"LastName","Strahl")
ADDPROPERTY(loObj,"FirstName","Rick")
*** Create initial JSON
*** { "lastname": "Strahl", "firstname": "Rick" }
lcJson = loSer.Serialize(loObj)
*** Now update the property names
loSer.MapPropertyName(@lcJson, "lastName","Last Name")
loSer.MapPropertyName(@lcJson, "firstName","First Name")
*** { "Last Name": "Strahl", "First Name": "Rick" }
? lcJson
Arrays, Collections and wwCollection objects
wwJsonSerializer serializes FoxPro arrays, Collections and wwCollection objects into JSON arrays. Key Value collections or wwKeyValue collections are rendered as JSON object maps where the keys are mapped to JSON properties of the object.
Array elements can be of any other type supported including object and other arrays. Arrays can be of a single type or mixed types as well.
Arrays are serialized as FoxPro Collections, which have a count property and an Item(x) method to retrieve items.
Here's a simple string array:
lcJson = '["Value,1","Value2. This \"is\" neat","Value3"]'
? lcJson
loResult = loJson.Deserialize(lcJson)
*** Result is a Collection
? loResult.Count
FOR lnX = 1 TO loResult.Count
? loResult.Item(lnX)
ENDFOR
? "*** Re-serialize String collection"
? loJson.Serialize(loResult) && Same as original string
Another example using an object array:
? "*** Object array"
lcJson = '[{"name":"Rick","company":"West Wind","Balance":100.05},' + ;
'{"name":"Markus","company":"EPS Software","Balance":1100.05},' +;
'{"name":"Kevin","company":"OakLeaf","Balance":1300.05},'
loResult =loJson.Deserialize(lcJson)
? loResult.Count
FOR lnX = 1 TO loResult.Count
loCust = loResult.Item(lnX)
? loCust.name, loCust.company, loCust.Balance
ENDFOR
? "*** re-serialize object array"
? loJson.Serialize(loResult) && same as above
You can also have arrays with mixed items: simple values, objects and even other objects.
? "*** Mixed items"
lcJson = '["Value" , {"name":"Rick","company":"West Wind"} , ["rick","markus"],"Test",10,null,"Test1"]'
?lcJson
loResult =loJson.Deserialize(lcJson)
? loResult.Count
FOR lnX = 1 TO loResult.Count
? loResult.Item(lnX)
ENDFOR
? "*** Re-serialize loResult"
? loJson.Serialize(loResult)
FoxPro Cursors or Tables
You can also serialize cursors, which are rendered as JSON arrays. These cursor arrays can also be attached as child arrays to objects.
To serialize a cursor you use a special syntax in the call to Serialize by using cursor:Alias
:
SELECT * FROM customers ;
INTO CURSOR TCompanies ;
ORDER BY company
loSerializer = CREATEOBJECT("wwJsonSerializer")
loSerializer.FormattedOutput = .T.
loSerializer.PropertyNameOverrides = "firstName,lastName,shipAddr,billRate"
*** Notice the 'cursor:alias' syntax
lcJSON = loSerializer.Serialize("cursor:TCompanies")
The result of this serialization looks like this:
[
{
"id": "_4FG12Y7U6",
"firstName": "John",
"lastName": "Doe",
"company": "Avionics Inc.",
"careof": "John Doe",
"address": "PO Box 1907\r\nManassas, VA 22110-0801",
"shipAddr": "",
"email": "",
"phone": "808 123-0001",
"billRate": 150,
"btype": "Labor_PRG",
"entered": "2013-06-12T09:46:40Z",
"state": ""
},
{
"id": "_4FG12Y7TX",
"firstName": "Steve",
"lastName": "Herbin",
"company": "Avis World Headquarters",
"careof": "Steve Herbin",
"address": "111 Old Country Road\nGarden City, NY 11530\n",
"shipAddr": "",
"email": "",
"phone": "(516) 222-3743",
"billRate": 55,
"btype": "Labor_PRG",
"entered": "2013-09-10T09:46:40Z",
"state": ""
},
{
"id": "_4FG12Y7TQ",
"firstName": "Dan",
"lastName": "Mullen",
"company": "XVC Solutions",
"careof": "Dan Mullen",
"address": "22xx Tudor Lane\r\nStow, OH 44xx",
"shipAddr": "",
"email": "",
"phone": "",
"billRate": 80,
"btype": "Labor_PRG",
"entered": "2013-10-19T09:46:40Z",
"state": ""
},
{
"id": "_4FG12Y7U4",
"firstName": "Kim",
"lastName": "Westin",
"company": "XYZ Systems house",
"careof": "",
"address": "9411 Broadway\r\nNew York, NY 11111",
"shipAddr": "",
"email": "",
"phone": "444-4444",
"billRate": 150,
"btype": "Labor_PRG",
"entered": "2013-07-02T09:46:40Z",
"state": ""
}
]
As with object output, you can use .PropertyNameOverrides
to override the names and case of the property names.
Legacy Format
The format of the default
cursor:cursorName
rendering has changed to return just the array. The legacy format returned a top level object withRows
array andCount
property. You can still use this format by usingcursor_legacy:cursorName
.
Mixing Objects and Cursors
You can also nest cursors into objects by attaching cursors as 'properties' to an object. In FoxPro you can reference the cursor by name using cursor:<alias>
as the value assigned to the property. When the object is serialized the serializer then serializes the cursor as a object array at the property.
An example of this is to create a custom JSON response for something like jqGrid, which requires an object structure and an array structure for the data to display. The following code demonstrates how to create this structure:
SELECT Company, Careof FROM wwDemo\tt_cust INTO CURSOR TPersons
loGrid = CREATE("EMPTY")
AddProperty(loGrid,"page",1)
AddProperty(loGrid,"total", Reccount())
AddProperty(loGrid,"records",10)
*** Attach the cursor as an Array property
AddProperty(loGrid,"rows","cursor:TPersons")
loSer = CREATEOBJECT("wwJsonSerializer")
loSer.FormattedOutput = .T.
lcJSON = loSer.Serialize(loGrid)
ShowText(lcJson)
This creates a structure like this:
{
page: 1,
total: 100,
records: 10,
rows: [
{
company: "West Wind",
careof: "Rick"
},
{
company: "EPS",
careof: "Markus"
}
]
}
Note the rows property and the assignment of cursor:TQuery
which generates just the array in the object without the Rows object prefix.
No built-in Two Way Cursor Serialization
The JSON .Deserialize()
method only works with objects and can't deserialize cursors directly. While you can serialize cursors, there's no way to turn JSON back into a cursor. Because a cursor is serialized as a JSON array, when you deserialize the JSON array you get back a FoxPro collection.
There are a couple of ways however that you can deserialize cursors explicitly:
.DeserializeCursor()
- Use
CollectionToCursor()
to convert a result collection to cursor
DeserializeCursor
This method is a special helper method that works on top level cursors - or object arrays - in the JSON data. This method is essentially a wrapper around .Deserialize()
and CursorToCollection()
.
To use it you pass in the top level JSON containing an object array (cursor data) and the name of an open cursor that contains the proper structure to import to.
DO wwJsonSerializer
loSer = CREATEOBJECT("wwJsonSerializer")
*** Some data to serialize
SELECT TOP 5 * FROM Customers ORDER BY Company INTO CURSOR TQuery
CLEAR
lcJson = loSer.Serialize("cursor:TQuery", .T.)
? lcJson
USE IN TQuery && close
*** Create an empty writable cursor as 'schema' to import to
SELECT * FROM Customers WHERE 1=0 INTO CURSOR TCustomers READWRITE
* Collection of cust objects
? loSer.DeserializeCursor(lcJson, "TCustomers")
BROWSE NOWAIT
CollectionToCursor()
As mentioned the JSON created from a serialized cursor is a JSON Object Array, and when you use standard de-serialization via DeSerialize()
the result is a collection object. You can take the collection result and run it through the CollectionToCursor()
library function (in wwutils.prg) to convert the collection to a cursor, by providing an open cursor into which to import the data. The open cursor's schema determines which fields and types that are imported into the cursor, matching JSON object properties to cursor field names.
To do this manually in FoxPro code looks like this:
*** Some data to serialize
SELECT * FROM customers INTO CURSOR TCompanies
*** Capture JSON from cursor: JSON Array
lcJSON = loSerializer.Serialize("cursor:TCompanies")
CLOSE DATA
*** Deserialize JSON Array into Collection
loCol = loSerializer.Deserialize(lcJson)
*** Schema: Create an empty, writable cursor to import to
SELECT * from Customers ;
INTO Cursor TCompanies READWRITE ;
WHERE .f.
*** Import data from the Collection
CollectionToCursor(loCol,"TCompanies")
BROWSE NOWAIT
While DeserializeCursor()
is easier to use and simply a wrapper around Deserialize()
and CollectionToCursor()
, this manual approach allows a lot more control. For example, you can apply this approach to any collection including child object properties in a nested object returned from JSON parsing (ie. import from Invoice.LineItems[]
).
© West Wind Technologies, 2023 • Updated: 01/19/22
Comment or report problem with topic