JSON Tools for Pascal
I decided to write a small yet capable JSON parser and builder. A JSON parser converts plain text in Javascript object notation format into a set of nodes which can be queried. A JSON builder allows you to dynamically create and manipulate nodes, then output those nodes as valid JSON text.Update
An update to JsonTools has been posted to its github repository. This update fixes a problem with escaped double quotes "\"" and adds a few conveniences. The most significant convenience is the addition of the Force method.The Force method allows users to add or modify a chain of objects, even if none exists. Force will search a path given, and return a node. If any part of the path doesn't exist, it will be created for you.
{ Force a series of nodes to exist and return the end node }
function
Force(
const
Path:
string
): TJsonNode;
var
N, C: TJsonNode;
begin
N := TJsonNode
.
Create;
N
.
Force(
'customer/first'
).AsString :=
'James'
;
N
.
Force(
'customer/last'
).AsString :=
'Milligan'
;
N
.
Force(
'address/street'
).AsString :=
'123 Skippy Lane'
;
N
.
Force(
'address/state'
).AsString :=
'FL'
;
C := N
.
Force(
'cart'
).Add;
C
.
Force(
'name'
).AsString :=
'shorts'
;
C
.
Force(
'price'
).AsNumber :=
25.12
;
N
.
Force(
'cart'
).Add
.
Add(
'name'
,
'gloves'
).Parent
.
Add(
'price'
,
30
);
N
.
Force(
'total'
).AsNumber :=
55.12
;
WriteLn(N
.
Value);
N
.
Free;
end
;
{ "customer": { "first": "James", "last": "Milligan" }, "address": { "street": "123 Skippy Lane", "state": "FL" }, "cart": [ { "name": "shorts", "price": 25.12 }, { "name": "gloves", "price": 30 } ], "total": 55.12 }Another addition is the Exists method. Exist will check a path and return True if a node is found matching the given path.
{ Search for a node using a path string and return true if exists }
function
Exists(
const
Path:
string
): Boolean;
{ Convert to an array and add an item }
function
Add: TJsonNode;
overload
;
{ Search for a node using a path string and return true if exists }
function
Find(
const
Path:
string
; out Node: TJsonNode): Boolean;
overload
;
Why use JSON?
JSON is a standardized text data format that has increasingly becoming ubiquitous. It works on any computer system, its portable, its concise, and its human readable and editable. Many web based API calls require either JSON content as post data, or return JSON content as results. Many desktop systems are switching to JSON as a file format for configurations, settings, layouts, or other data persisted to disk.Why write a JSON parser when one already exists?
The reason we wrote this parser and builder comes down to two primary reasons.- we wanted a very simple interface to work with JSON in Pascal that works in the manner to which we are accustomed. More on this below.
- We wanted to write a fairly small yet complete solution cleanly implemented in one class with no dependencies other than the SysUtils and Classes units.
How does our JSON parser make working with JSON easier?
Our parser is very easy to use. Simply add the JsonTools unit to your uses clause, then you can parse a valid JSON document like so:N := TJsonNode
.
Create;
N
.
Parse(YourJsonText);
N
.
Free;
N
.
Value := YourJsonText;
N
.
LoadFromFile(YourJsonFileName);
WriteLn(N
.
Value);
N
.
Value :=
'[ "Hello", "World!" ]'
;
for
C
in
N
do
WriteLn(C
.
Value);
WriteLn(N
.
Value);
"Hello" "World!" [ "Hello", "World!" ]If you are unsure if your source JSON text is valid, you can write this bit of code to check if it is correct:
S :=
'{ this is not valid json }'
;
if
N
.
TryParse(S)
then
// Do something with your data
else
// Your input did not contain valid JSON text
More things you can do with JSON
Now that we know how this JSON object can parse text, let's look at an example of how it can navigate and manipulate JSON nodes. Here is a bit of JSON text data we'll be using in a few examples to follow.{
"name"
:
"Alice Brown"
,
"sku"
:
"54321"
,
"price"
: 199.95,
"shipTo"
: {
"name"
:
"Bob Brown"
,
"address"
:
"456 Oak Lane"
,
"city"
:
"Pretendville"
,
"state"
:
"HI"
,
"zip"
:
"98999"
},
"billTo"
: {
"name"
:
"Alice Brown"
,
"address"
:
"456 Oak Lane"
,
"city"
:
"Pretendville"
,
"state"
:
"HI"
,
"zip"
:
"98999"
}
}
N
.
LoadFromFile(
'orders.json'
);
WriteLn(
'The price is '
, N
.
Find(
'price'
).Value);
WriteLn(
'The customer is '
, N
.
Find(
'billTo/name'
).Value);
The price is 199.95 The customer is "Alice Brown"If you wanted to change the shipTo to match the billTo, you could simply write:
N
.
Find(
'shipTo'
).Value := N
.
Find(
'billTo'
).Value;
If you just want to change the name of the customer you could write:
N
.
Find(
'billTo/name'
).AsString :=
'Joe Schmoe'
;
If you do not want to use AsString would need to write this statement instead, which makes 'Joe Schmoe' a valid JSON string:
N
.
Find(
'billTo/name'
).Value :=
'"Joe Schmoe"'
;
Here are some examples of valid values you can use when setting the Value property of a node.
N
.
Value :=
'true'
;
// node becomes a bool node
N
.
Value :=
'3.1415'
;
// node becomes a number node
N
.
Value :=
'null'
;
// node becomes a null node
N
.
Value :=
'[]'
;
// node becomes an array
N
.
Value :=
'{}'
;
// node becomes an object
Alternately, the five statements below do the exact same thing as the five statements above, but with a bit of added type safety:
N
.
AsBoolean := True;
N
.
AsNumber :=
3.1415
;
N
.
AsNull;
N
.
AsArray;
N
.
AsObject;
Switching to another node
As noted in the last section, attempting to set the root node to a value that is not an object or an array is an error. The JSON standard dictates that the root level of all JSON objects must either be an object or an arrayTo switch to another node you might write:
N := N
.
Find(
'shipTo/name'
);
N
.
Add(
'first'
,
'Joe'
);
N
.
Add(
'last'
,
'Schmoe'
);
WriteLn(N
.
Value);
N := N
.
Root;
{ "first": "Joe", "last": "Schmoe" }
Adding and removing nodes
Nodes can be added or removed programatically using special purpose methods. If you want to add a value to an object (or an array) you can write:N
.
Add(
'note'
,
'Customer requested we change the ship to address'
);
"note"
:
"Customer requested we change the ship to address"
Also notice when we use the Add method we do not need to use JSON strings. We can just use normal strings and they will be converted to JSON format internally. This internal formatting allows us to add strings with multiple lines or other format encoding, and internally they will be retained as JSON compatible strings.
Add is overloaded allow you to add bool, number, string, and more:
N
.
Add(
'discount'
, -
8.50
);
// adds a number node
N
.
Add(
'verified'
, True);
// adds a bool node
N
.
Add(
'wishlist'
, nkArray);
// adds an array node
A child node can be removed either using the Delete method or Clear method.
N
.
Delete(
'note'
);
// removes the note node
N
.
Delete(
'shipTo'
);
// removes the shipTo node
N
.
ToArray
.
Add(
'note'
,
'Please call the customer'
);
N
.
Delete(
'0'
);
To remove all child nodes of an object or array, simply type:
N
.
Clear;
The basic properties of every JSON node
Every node at its core stores the following information:property
Root: TJsonNode
// the root of your JSON document (read only)
property
Parent: TJsonNode
// the parent of your JSON node (read only)
property
Kind: TJsonNodeKind
// the kind of node, such as object, array, string etc (read write)
property
Name:
string
// the name of node (read write)
property
Value:
string
// the value of the node in JSON form (read write)
nkObject, nkArray, nkBool, nkNull, nkNumber, nkString
If a duplicate Name is set, then the existing node of the same name is removed and replaced by the current node. You can only set the name of a node if the parent is an object, otherwise any attempts to change the name are ignored.
When Value is set, internally the value you pass is parsed as JSON and the entire value portion of the node is replaced, potentially changing the node kind in the process. It is important to note that the root node value must be a valid JSON object or array. It cannot be any other kind.
Here are a few examples of how the Value property can be used.
N
.
Child(
'sku'
).Value :=
'[ "54321", "98765" ]'
;
// sku becomes an array of values
N
.
Find(
'shipTo/name'
).Value :=
'{ "id": 5028, "primary": true }'
;
// name becomes object
function
Child(Index: Integer): TJsonNode;
// get a child node by index
function
Child(
const
Name:
string
): TJsonNode;
// get a child node by name
property
Count: Integer;
// return the number of child nodes
Source code
All the source code to this parse if freely available under the GPLv3 license. It's hosted on github here and the interface source is listed here for your reference. Feel free to send me your comments or feedback.type
TJsonNode =
class
public
{ A parent node owns all children. Only destroy a node if it has no parent.
To destroy a child node use Delete or Clear methods instead. }
destructor
Destroy;
override
;
{ GetEnumerator adds 'for ... in' statement support }
function
GetEnumerator: TJsonNodeEnumerator;
{ Loading and saving methods }
procedure
LoadFromStream(Stream: TStream);
procedure
SaveToStream(Stream: TStream);
procedure
LoadFromFile(
const
FileName:
string
);
procedure
SaveToFile(
const
FileName:
string
);
{ Convert a json string into a value or a collection of nodes. If the
current node is root then the json must be an array or object. }
procedure
Parse(
const
Json:
string
);
{ The same as Parse, but returns true if no exception is caught }
function
TryParse(
const
Json:
string
): Boolean;
{ Add a child node by node kind. If the current node is an array then the
name parameter will be discarded. If the current node is not an array or
object the Add methods will convert the node to an object and discard
its current value.
Note: If the current node is an object then adding an existing name will
overwrite the matching child node instead of adding. }
function
Add(
const
Name:
string
; K: TJsonNodeKind = nkObject): TJsonNode;
overload
;
function
Add(
const
Name:
string
; B: Boolean): TJsonNode;
overload
;
function
Add(
const
Name:
string
;
const
N: Double): TJsonNode;
overload
;
function
Add(
const
Name:
string
;
const
S:
string
): TJsonNode;
overload
;
{ Convert to an array and add an item }
function
Add: TJsonNode;
overload
;
{ Delete a child node by index or name }
procedure
Delete(Index: Integer);
overload
;
procedure
Delete(
const
Name:
string
);
overload
;
{ Remove all child nodes }
procedure
Clear;
{ Get a child node by index. EJsonException is raised if node is not an
array or object or if the index is out of bounds.
See also: Count }
function
Child(Index: Integer): TJsonNode;
overload
;
{ Get a child node by name. If no node is found nil will be returned. }
function
Child(
const
Name:
string
): TJsonNode;
overload
;
{ Search for a node using a path string and return true if exists }
function
Exists(
const
Path:
string
): Boolean;
{ Search for a node using a path string }
function
Find(
const
Path:
string
): TJsonNode;
overload
;
{ Search for a node using a path string and return true if exists }
function
Find(
const
Path:
string
; out Node: TJsonNode): Boolean;
overload
;
{ Force a series of nodes to exist and return the end node }
function
Force(
const
Path:
string
): TJsonNode;
{ Format the node and all its children as json }
function
ToString:
string
;
override
;
{ Root node is read only. A node the root when it has no parent. }
property
Root: TJsonNode
read
GetRoot;
{ Parent node is read only }
property
Parent: TJsonNode
read
FParent;
{ Kind can also be changed using the As methods.
Note: Changes to Kind cause Value to be reset to a default value. }
property
Kind: TJsonNodeKind
read
FKind
write
SetKind;
{ Name is unique within the scope }
property
Name:
string
read
GetName
write
SetName;
{ Value of the node in json e.g. '[]', '"hello\nworld!"', 'true', or '1.23e2' }
property
Value:
string
read
GetValue
write
Parse;
{ The number of child nodes. If node is not an object or array this
property will return 0. }
property
Count: Integer
read
GetCount;
{ AsJson is the more efficient version of Value. Text returned from AsJson
is the most compact representation of the node in json form.
Note: If you are writing a services to transmit or receive json data then
use AsJson. If you want friendly human readable text use Value. }
property
AsJson:
string
read
GetAsJson
write
Parse;
{ Convert the node to an array }
property
AsArray: TJsonNode
read
GetAsArray;
{ Convert the node to an object }
property
AsObject: TJsonNode
read
GetAsObject;
{ Convert the node to null }
property
AsNull: TJsonNode
read
GetAsNull;
{ Convert the node to a bool }
property
AsBoolean: Boolean
read
GetAsBoolean
write
SetAsBoolean;
{ Convert the node to a string }
property
AsString:
string
read
GetAsString
write
SetAsString;
{ Convert the node to a number }
property
AsNumber: Double
read
GetAsNumber
write
SetAsNumber;
end
;
Comparisons
We've added a new page to test our our new parser in comparison to several other pascal based parsers. You can read more about these tests here.The results show that our parser is dramatically better than the other parsers we were able to find and test. Better both in terms of speed and correctness. Though we believe our test are fair, they are by no means exhaustive, some please be mindful of that fact when considering our results.