Foundation · Libraries · Uncategorized

Efficient and easy-to-use JSON and BSON library

We share our efficient and easy-to-use JSON and BSON library that supports a Document Object Model (DOM), direct reading and writing in JSON and BSON format, and automatic serialization of Delphi records and classes to and from JSON and BSON.

Why another JSON library?

There are already quite a few JSON libraries out there, so why create another one? For our grijjy app and BAAS, we needed a library that supports all of the features below. Other libraries have mixed support for these features:

  • In addition to JSON, our library supports BSON as a first class citizen, not as an afterthought. BSON is a binary version of JSON that was initially designed as a storage format for the MongoDB database.
  • In concert with that, our library supports MongoDB Extended JSON. In addition to “strict” JSON, it supports reading and writing JSON in “Mongo Shell” mode (for example, keys in objects don’t always require quotes).
  • Our library is also pretty efficient. It is not the fastest JSON library out there, but it is up to 12 times faster than Delphi’s built-in JSON classes, using up to 70% less memory.
  • The library is easy-to-use and well documented. Memory management is automatic. Creating a BSON DOM is similar to using a list or dictionary.
  • The library works on all platforms supported by Delphi.
  • Our library supports 3 API levels, allowing you to choose the best one for the job:
    • You can use a DOM-style API to create a document in memory, parse a JSON string into a document, load a JSON/BSON file into a document, or save a document in JSON/BSON format.
    • For optimum speed and memory efficiency, you can use reader and writer classes to directly read from or write to JSON/BSON format.
    • Finally, our library has a powerful serialization engine that can load and save Delphi records and objects in JSON/BSON format. That can often reduce your JSON/BSON code to a one-liner.

The API is inspired by the C# MongoDB driver, so it may feel familiar if you use this driver in .NET projects.

Even though our engine works well with MongoDB, it has no dependencies on MongoDB whatsoever. It can be used as a full featured stand-alone JSON engine. If you later need support for BSON as well, then adding it will be a no-brainer.

This library is part of the Grijjy Foundation repository and contained in the units in the Grijjy.Bson unit scope. Like the other Grijjy Foundation code, the library is licensed under the Simplified BSD License. See License.txt for details.

In the remainder of this article, I will introduce the 3 API levels. This post is quite lengthy. However, that is no indicator of the complexity of the library. It’s just that the library has a lot of features that I don’t want to just skim over. You can pick and choose the features you are interested in…

1. DOM API

The Document Object Model API may be your go-to API for working with JSON and BSON. If you have been using Delphi’s built-in JSON or XML classes, then you are already familiar with using a DOM. Our API and implementation is different though.

DOM Example

Let’s start with a quick example of building a DOM:

var
  Doc: TgoBsonDocument;
  A: TgoBsonArray;
  Json: String;
  Bson: TBytes;
begin
  Doc := TgoBsonDocument.Create;
  Doc.Add('Hello', 'World');

  A := TgoBsonArray.Create(['awesome', 5.05, 1986]);
  Doc.Add('BSON', A);

  Json := Doc.ToJson; // Returns:
  // { "hello" : "world",
  //   "BSON": ["awesone", 5.05, 1986] }

  Bson := Doc.ToBson; // Saves to binary BSON

  Doc := TgoBsonDocument.Parse('{ "Answer" : 42 }');
  WriteLn(Doc['Answer']); // Outputs 42
  Doc['Answer'] := 'Unknown';
  Doc['Pi'] := 3.14;
  WriteLn(Doc.ToJson); // Outputs:
// { "Answer" : "Unknown", "Pi" : 3.14 }
end;

The root type in the BSON object model is TgoBsonValue. TgoBsonValue is a record type which can hold any type of BSON value.

Most types in the library start with a TgoBson prefix. Although this may suggest that this only applies to BSON and not JSON, that is not the case. We choose this prefix because BSON supports more data types than JSON (you could say that BSON is a superset of JSON), so it is more accurate. The types are agnostics to the actual storage format and can be used with both JSON and BSON.

Some implicit class operators make it easy to assign basic types:

var
  Value: TgoBsonValue;
begin
  Value := True;                      // Creates a Boolean value
  Value := 1;                         // Creates an Integer value
  Value := 3.14;                      // Creates a Double value
  Value := 'Foo';                     // Creates a String value
  Value := TBytes.Create(1, 2, 3);    // Creates a binary (TBytes) value
  Value := TgoObjectId.GenerateNewId; // Creates an ObjectId value
end;

Note that you can change the type later by assigning a value of another type. You can also go the other way around:

var
  Value: TgoBsonValue;
  FloatVal: Double;
begin
  Value := 3.14;              // Creates a Double value
  FloatVal := Value;          // Uses implicit cast
  FloatVal := Value.AsDouble; // Or more explicit cast
  Value := 42;                // Creates an Integer value
  FloatVal := Value;          // Converts an Integer BSON value to a Double
  FloatVal := Value.AsDouble; // Raises exception because types don't match exactly

  if (Value.BsonType = TgoBsonType.Double) then
    FloatVal := Value.AsDouble; // Now it is safe to cast

  // Or identical:
  if (Value.IsDouble) then
    FloatVal := Value.AsDouble;
end;

Note that the implicit operators will try to convert if the types don’t match exactly. For example, a BSON value containing an Integer value can be implicitly converted to a Double. If the conversion fails, it returns a zero value (or empty string).

The As* methods however will raise an exception if the types don’t match exactly (like the as-operator does in Delphi). You should use these methods if you know that the type you request matches the value’s type exactly. These methods are a bit more efficient than the implicit operators.

You can check the value type using the BsonType-property or one of the Is* methods.

For non-basic types, there are value types that are “derived” from TgoBsonValue:

  • TgoBsonNull: the special “null” value.
  • TgoBsonArray: an array of other BSON values.
  • TgoBsonDocument: a document containing key/value pairs, where the key is a string and the value can be any BSON value. A document is similar to a dictionary in many programming languages, or the “object” type in JSON.
  • TgoBsonBinaryData: arbitrary binary data. Is also used to store GUID’s (but not ObjectId’s).
  • TgoBsonDateTime: a date/time value with support for conversion to and from UTC (Universal) time. Is always stored in UTC format (as the number of UTC milliseconds since the Unix epoch).
  • TgoBsonRegularExpression: a regular expression with options.
  • TgoBsonJavaScript: a piece of JavaScript code.
  • TgoBsonJavaScriptWithScope: a piece of JavaScript code with a scope (a set of variables with values, as defined in another document).
  • TgoBsonTimestamp: special internal type used by MongoDB for replication and sharding.
  • TgoBsonMaxKey: special type which compares higher than all other possible BSON element values.
  • TgoBsonMinKey: special type which compares lower than all other possible BSON element values.
  • TgoBsonUndefined: an undefined value (deprecated by BSON).
  • TgoBsonSymbol: a symbol from a lookup table (deprecated by BSON).

Note that these are not “real” derived types, since they are implemented as Delphi records (which do not support inheritance). But the implicit operators make it possible to treat each of these types as a TgoBsonValue. For example

var
  MyArray: TgoBsonArray;
  Value: TgoBsonValue;
begin
  MyArray := TgoBsonArray.Create([1, 3.14, 'Foo', False]);
  Value := MyArray; // "subtypes" are compatible with TgoBsonValue

  // Or shorter:
  Value := TgoBsonArray.Create([1, 3.14, 'Foo', False]);
end;

Arrays

The example above also shows that arrays can be created very easily. An array contains a collection of BSON values of any type. Since BSON values can be implicitly created from basic types, you can pass multiple types in the array constructor. In the example above, 4 BSON values will be added to the array of types Integer, Double, String and Boolean.

You can also add items using the Add or AddRange methods:

MyArray := TgoBsonArray.Create;
MyArray.Add(1);
MyArray.Add(3.14);
MyArray.Add('Foo');

Some methods return the array (or document) itself, so they can be used for chaining (aka as a “fluent interface”). The example above is equivalent to:

MyArray := TgoBsonArray.Create;
MyArray.Add(1).Add(3.14).Add('Foo');

You can change values (and types) like this:

// Changes entry 1 from Double to Boolean
MyArray[1] := True;

Documents

Documents (or dictionaries) can also be created easily:

var
  Doc: TgoBsonDocument;
begin
  Doc := TgoBsonDocument.Create('Answer', 42);
end;

This creates a document with a single entry called Answer with a value of 42. Keep in mind that the value can be any BSON type:

Doc := TgoBsonDocument.Create('Answer', TgoBsonArray.Create([42, False]));

You can Add, Remove and Delete (Adds can be fluent):

Doc := TgoBsonDocument.Create;
Doc.Add('Answer', 42);
Doc.Add('Pi', 3.14).Add('Pie', 'Yummi');

// Deletes second item (Pi):
Doc.Delete(1);

// Removes first item (Answer):
Doc.Remove('Answer');

Like Delphi dictionaries, the Add method will raise an exception if an item with the given name already exists. Unlike Delphi dictionaries however, you can easily set an item using its default accessor:

// Adds Answer:
Doc['Answer'] := 42;

// Adds Pi:
Doc['Pi'] := 3.14;

// Updates Answer:
Doc['Answer'] := 'Everything';

This adds the item if it does not yet exists, or replaces it otherwise (there is no (need for an) AddOrSet method).

Also unlike Delphi dictionaries, documents maintain insertion order and you can also access the items by index:

// Returns item by name:
V := Doc['Pi'];

// Returns item by index:
V := Doc.Values[1];

Documents can be easily parsed from JSON:

Doc := TgoBsonDocument.Parse('{ "Answer" : 42 }');

The parser understands standard JSON as well as the MongoDB JSON extensions.

You can also load a document from a BSON byte array:

Bytes := LoadSomeBSONData();
Doc := TgoBsonDocument.Load(Bytes);

These methods will raise exceptions if the JSON or BSON data is invalid.

Memory Management

All memory management in this library is automatic. You never need to (and you never must) destroy any objects yourself.

The object model types (TgoBsonValue and friends) are all Delphi records. The actual implementations of these records use interfaces to manage memory.

There is no concept of ownership in the object model. An array does not own its items and a document does not own its elements. So you are free to add the same value to multiple arrays and/or documents without ownership concerns.

Importing and exporting JSON and BSON

For easy storing, all BSON values have methods called ToJson and ToBson to store its value into JSON or BSON format:

var
  A: TgoBsonValue;
  B: TBytes;
begin
  A := 42;
  WriteLn(A.ToJson); // Outputs '42'

  A := 'Foo';
  WriteLn(A.ToJson); // Outputs '"Foo"'

  A := TgoBsonArray.Create([1, 'Foo', True]);
  WriteLn(A.ToJson); // Outputs '[1, "Foo", true]'

  A := TgoBsonDocument.Create('Pi', 3.14);
  WriteLn(A.ToJson); // Outputs '{ "Pi" : 3.14 }'
  B := A.ToBson;     // Outputs document in BSON format
end;

When outputting to JSON, you can optionally supply a settings record to customize the output:

  • Whether to pretty-print the output
  • What strings to use for indentation and line breaks
  • Whether to output standard JSON or use the MongoDB shell syntax extension

If you don’s supply any settings, then output will be in Strict JSON format without pretty printing.

Easy loading is only supported at the Value, Document and Array level, using the static Parse and Load methods:

var
  Doc: TgoBsonDocument;
  Bytes: TBytes;
begin
  Doc := TgoBsonDocument.Parse('{ "Pi" : 3.14 }');
  Bytes := LoadSomeBSONData();
  Doc := TgoBsonDocument.Load(Bytes);
end;

You can load other types using the IgoJsonReader and IgoBsonReader interfaces, which we will discuss in the next section.

For more information, consult the API documentation for the DOM API level.

2. Reader/Writer API

The second API level allows you to directly read or write JSON or BSON data. This is the fastest way to create and consume JSON/BSON, but takes a bit more work, especially on the reading side.

The JSON reader and writer supports both the “strict” JSON syntax, as well as the “Mongo Shell” syntax. This library supports all the current extensions, as well as some deprecated legacy extensions. The JSON reader accepts both key names with double quotes (as per JSON spec) and without quotes.

Writer Example

Let’s start with a simple example. Consider this JSON document:

{ "x" : 1,
  "y" : 2,
  "z" : [ 3.14, true] }

You can use a writer to save it to BSON like this:

var
  Writer: IgoBsonWriter;
  Bson: TBytes;
begin
  Writer := TgoBsonWriter.Create;
  Writer.WriteStartDocument;

  Writer.WriteName('x');
  Writer.WriteInt32(1);

  // Writes name and value in single call:
  Writer.WriteInt32('y', 2); 

  Writer.WriteName('z');
  Writer.WriteStartArray;
  Writer.WriteDouble(3.14);
  Writer.WriteBoolean(True);
  Writer.WriteEndArray;

  Writer.WriteEndDocument;

  Bson := Writer.ToBson;
end;

Likewise, you can save to JSON by using the IgoJsonWriter interface instead. Both these interfaces derive from IgoBsonBaseWriter, so they have the same base API.

You can also write any value (including complete arrays in documents) in a single call:

var
  Document: TgrBsonDocument;
  Writer: IgoJsonWriter;
  Json: String;
begin
  Document := CreateSomeDocument();
  Writer := TgoJsonWriter.Create;
  Writer.WriteValue(Document);
  // or Writer.WriteDocument(Document);
  Json := Writer.ToJson;
end;

This is equivalent to:

var
  Document: TgrBsonDocument;
  Json: String;
begin
  Document := CreateSomeDocument();
  Json := Document.ToJson;
end;

In fact, the DOM API uses readers and writers to load and save JSON/BSON data.

Reader Example

You can also manually read data by using the IgoBsonReader and IgoJsonReader interfaces. If you know the structure of the input, then reading just mirrors writing:

var
  Reader: IgoBsonReader;
  Name: String;
  Value: Integer;
begin
  Reader := TgoBsonReader.Load('sample.bson');

  Reader.ReadStartDocument;
  Name := Reader.ReadName;
  Value := Reader.ReadInt32;

  // etc...

  Reader.ReadEndDocument;
end;

The Read* methods will raise an exception if the structure of the input is different than what you expect (for example, when you call ReadInt32, but the current position in the input contains a string).

Since you usually you don’t know the structure of the input in advance, it is safer to check the current type before reading. A typical read loop thus looks like this:

while (not Reader.EndOfStream) do
begin
  case Reader.GetCurrentBsonType of
    TgrBsonType.Int32: Reader.ReadInt32;
    TgrBsonType.Double: Reader.ReadDouble;
    etc...
  end;
end;

But you may find it easier to just use the DOM API level for reading, or to use automatic serialization, which is presented next.

For more information, consult the API documentation for the Reader/Writer API level.

3. Serialization API

The final API level adds support for serializing Delphi records and objects to JSON and BSON.

Serialization example

Again, it is easiest to get started with an example:

type
  TOrderDetail = record
  public
    Product: String;
    Quantity: Integer;
  end;

  TOrder = record
  public
    Customer: String;
    OrderDetails: TArray<TOrderDetail>;
  end;

procedure TestSerialization;
var
  Order, Rehydrated: TOrder;
  Json: String;
begin
  Order.Customer := 'John';

  SetLength(Order.OrderDetails, 2);
  Order.OrderDetails[0].Product := 'Pen';
  Order.OrderDetails[0].Quantity := 1;
  Order.OrderDetails[1].Product := 'Ruler';
  Order.OrderDetails[1].Quantity := 2;

  { Serialize Order record to JSON: }
  TgoBsonSerializer.Serialize(Order, Json);
  WriteLn(Json); // Outputs:
  // { "Customer" : "John",
  //   "OrderDetails" : [
  //     { "Product" : "Pen", "Quantity" : 1 },
  //     { "Product" : "Ruler", "Quantity" : 2 }
  //   ]
  // }

  { Deserialize JSON to Order record: }
  TgoBsonSerializer.Deserialize(Json, Rehydrated);
  { Rehydrated will have the same values as Order }
end;

The actual JSON/BSON serialization code boils down to a single line of code now. It doesn’t get much easier than that.

So why are we only halfway through this article? The serialization engine is very flexible and has a lot of features to customize the output. The remainder of this article discusses those features. You may not need most of them, but it is good to know they are there in case you need them.

The observant reader may wonder how the Serialize and Deserialize methods know the type of the data that is serialized. They know this because these methods are actually generic methods, so the Serialize call can be rewritten like this:

TgoBsonSerializer.Serialize<TOrder>(Order, Json);

However, Delphi can often infer the generic type by the parameters passed to the method. In that case, you can omit the generic type argument (like <TOrder>) from the call and have Delphi figure it out.

Features

The serialization engine supports the following features:

  • You can serialize records and classes to JSON, BSON and TgoBsonDocument.
  • You can also serialize dynamic arrays to JSON (but not to BSON and TgoBsonDocument).
  • By default, all public fields and public and published read/write properties are serialized. Private and protected fields and properties are never serialized.
  • Fields can be of type Boolean, Integer (all sizes and flavors), floating-point (Single and Double), WideChar, UnicodeString, TDateTime, TGUID, TgoObjectId and TBytes (for binary data).
  • Fields can also be of an enumerated type, as long as that type does not have any explicitly declared values (since Delphi provides no RTTI for those).
  • Likewise, a set of an enumerated type is also supported.
  • Furthermore, a field can also be of a serializable record or class type, or a dynamic array of a serializable type. Static (fixed size) arrays are not supported.
  • You can customize some behavior and output using attributes.

Representations

By default, the serialization engine serializes fields and properties (collectively called “members” from here onward) as their native types. That is, integers are serialized as integers and strings are serialized as strings. However, you can use the BsonRepresentation attribute to change the way a member is serialized:

type
  TColor = (Red, Green, Blue);

type
  TOrderDetail = record
  public
    Color: TColor;

    [BsonRepresentation(TgoBsonRepresentation.String)]
    ColorAsString: TColor;
  end;

This serializes the Color member as a integer (which is the default serialization type for enums), but serializes the ColorAsString member as a string (using the name of the enum, eg “Red”, “Green” or “Blue”). Not all types can be serialized as other types. Below is a list of the types that can be serialized as another type, and the conversion that will take place.

Data type Can be serialized as
Boolean
  • Boolean (default)
  • Int32, Int64, Double (False=0, True=1)
  • String (False=”false”, True=”true”)
Integer
  • Int32, Int64 (default)
  • Double
  • String (IntToStr-conversion)
Enum
  • Int32 (default, ordinal value)
  • Int64 (ordinal value)
  • String (name of the enum value)
Set
  • Int32, Int64 (default, stored as a bitmask)
  • String (comma-separated list of elements, without any (square) brackets)
Floating-point
  • Double (default)
  • Int32, Int64 (truncated version)
  • String (FloatToStr-conversion, in US format)
TDateTime
  • DateTime (default)
  • Int64 (number of UTC ticks since midnight 1/1/0001, using 10,000 ticks per millisecond)
  • String (DateToISO8601-conversion)
  • Document (a document with two elements: TimeStamp serialized as a DateTime value, and Ticks serialized as the number of ticks since midnight 1/1/0001). For example: { "DateTime" : ISODate("2016-05-01T15:28:57.784Z"), "Ticks" : NumberLong("635977133377840000") }
String
  • String (default)
  • Symbol
  • ObjectId (if the string is a valid TgoObjectId)
WideChar
  • Int32 (default, ordinal value)
  • Int64 (ordinal value)
  • String (single-character string)
TGUID
  • Binary (default)
  • String (without curly braces)
TgoObjectId
  • TgoObjectId (default)
  • String (string value of ObjectId)
TBytes
  • Binary (default)
  • String (hex string, using 2 hex digits per byte)

Note that for array members, the BsonRepresentation attribute applies to the element types, not to the array itself:

type
  TColor = (Red, Green, Blue);

type
  TMyColors = record
  public
    [BsonRepresentation(TgoBsonRepresentation.String)]
    Colors: TArray<TColor>;
  end;

This will serialize each color as a string (not the entire array as a string).

Handling Extra Elements

When a JSON/BSON stream is deserialized, the name of each element is used to look up a matching member in the record or class. Normally, if no matching member is found, the element is ignored. This also means that when the record or class is rendered back to JSON/BSON, those extra exlements will not exist and may be lost forever.

You can also treat extra members in the JSON/BSON stream as an error condition. In that case, an exception will be raised when extra elements are found. To enable this error, use the BsonErrorOnExtraElements attribute at the record or class level:

type
  [BsonErrorOnExtraElements]
  TOrderDetail = record
  public
    ...
  end;

Member Customization

Normally, read-only properties are not serialized (unless the property is of a class type, and the object property has already been created). If you want to serialize read-only properties, you can mark them with a BsonElement attribute:

type
  TOrder = class
  public
    [BsonElement]
    property TotalAmount: Double read GetTotalAmount;
  end;

Of course, read-only properties are never deserialized.

Also, you may wish to serialize a member using a different name than the member name. A common use for this is if you want to serialize using a C-style name (lower case with underscores) but you would like the member to have a Pascal-style name (with camel caps). Another situation where you may want to use this is if the serialization name includes a character that is invalid in a Delphi identifier. You can use the BsonElement attribute to provide the serialization name:

type
  TOrder = record
  public
    [BsonElement('customer_name')]
    CustomerName: String;

    [BsonElement('$id')]
    Id: TgoObjectId;
  end;

You may also choose to ignore a public member when serializing using the BsonIgnore attribute:

type
  TOrder = record
  public
    CustomerName: String;

    [BsonIgnore]
    CustomerAge: Integer;
  end;

This will only serialize the CustomerName member. This would be the same as making the CustomerAge field private or protected, with the difference that the CustomAge field is still accessible in code outside of the TOrder record.

You can also ignore a field only when it has a default value, using the BsonIgnoreIfDefault attribute:

type
  TOrder = record
  public
    [BsonIgnoreIfDefault]
    CustomerName: String;
  end;

This will only serialize the customer name if it is not an empty string. For other types the default value will be 0, False, [] etc. For boolean, integral and string types, you can specify the default value using the BsonDefaultValue attribute:

type
  TOrder = record
  public
    [BsonIgnoreIfDefault]
    [BsonDefaultValue('John Smith')]
    CustomerName: String;
  end;

This will only serialize the customer name if it isn’t ‘John Smith’.

Note: an exception will be raised if you apply the BsonDefaultValue attribute to a member that is not of a boolean, integral or string type.

The BsonIgnoreIfDefault attribute can be used on all types except record types.

Using Records

The easiest way to serialize to/from JSON/BSON is by declaring record types as shown above.

When a record is deserialized, all its values will be cleared first. This assures that no values will be left uninitialized if certain members are not deserialized.

If you want to customize the initialization behavior, then you can add a method called Initialize without parameters. If such a method exists, then it is called instead of clearing all fields:

type
  TOrder = record
  public
    // This method gets called before deserializing a TOrder
    procedure Initialize;
  end;

Using Classes

Serialization is easiest and most efficient when used with record types. You can also serialize objects (class instances), but need to be aware of a different behavior.

When you deserialize an object, and the object you pass in has a value of nil, then a new instance will be created. You are responsible for releasing the instance at some later point:

type
  TOrder = class
  public
    Customer: String;
  end;

procedure TestDeserialization;
var
  Order: TOrder;
begin
  Order := nil;
  TgoBsonSerializer.Deserialize('{ "Customer" : "John" }', Order);
end;

This will create a new TOrder instance and return it in the Order parameter. The TOrder instance is created by calling a parameterless constructor. If the TOrder class has constructor without parameters, then that constructor will be called. Otherwise, a parameterless constructor of the ancestor class will be used. If the ancestor class also doesn’t have a parameterless constructor, then we keep going up one ancestor in the chain, until TObject is reached, which always has a parameterless constructor.

If you pass a non-nil value to Deserialize, then the existing object will be updated and no new instance will be created.

Be sure to always initialize the object before passing it to Deserialize, either by setting it to nil or to an existing instance. When you fail to do this, the object pointer may contain garbage (on non-ARC platforms) and the call to Deserialize will probably result in an Access Violation at some point.

When deserializing a field or property of a class type, the behavior depends on whether the member is already assigned.

Deserializing Assigned object-properties

Usually, it is best to make sure that the member is always assigned, by creating it in the constructor and destroying it in the destructor:

type
  TOrderDetail = class
  ...
  end;

  TOrder = class
  private
    FCustomer: String;
    FDetail: TOrderDetail;
  public
    constructor Create;
    destructor Destroy; override;

    property Customer: String read FCustomer write FCustomer;
    property Detail: TOrderDetail read FDetail; // Read-only
  end;

constructor TOrder.Create;
begin
  inherited;
  FDetail := TOrderDetail.Create;
end;

destructor TOrder.Destroy;
begin
  FDetail.Free;
  inherited;
end;

This is a very common design pattern when using composition. Properties that are of a class type (like Detail in this example) are usually read-only.

When deserializing the TOrder.Detail property in this example, its members are deserialized as usual. Even though the Detail property is read-only, it will still be deserialized (other read-only properties are usually ignored).

Deserializing Non-Assigned object-properties

If the member is not assigned, it is only created and assigned if it is a read/write property (or field):

type
  TOrderDetail = class
  ...
  end;

  TOrder = class
  private
    FCustomer: String;
    FDetail: TOrderDetail;
  public
    property Customer: String read FCustomer write FCustomer;
    property Detail: TOrderDetail read FDetail write FDetail; // Read/write
  end;

In this case, when deserializing the Detail property, it will be created (using a parameterless constructor) and assigned to Detail. You need to make sure though that the Detail property will be destroyed at some point. You could make the TOrder class the owner and have it destroy the property in the destructor.

This design pattern is less common and not recommended. The recommended approach is to always make sure the Detail property is assigned (and read-only), as mentioned previously.

Polymorphism

A complication that arises when serializing classes (instead of records) it that they may be part of a class hierarchy:

type
  TAnimal = class
  public
    Weight: Double;
  end;

  TDog = class(TAnimal)
  public
    FurColor: String;
  end;

All animals have a weight, but only dogs have fur. When serializing a TDog, the output is as expected:

var
  Dog: TDog;
  Json: String;
begin
  Dog.Weight := 30;
  Dog.FurColor := 'Blond';
  TgoBsonSerializer.Serialize(Dog, Json); // Result:
  // { "Weight" : 30.0, "FurColor" : "Blond" }
end;

However, output is different when a TDog is serialized as a TAnimal:

var
  Dog: TDog;
  Animal, Rehydrated: TAnimal;
  Json: String;
begin
  Dog.Weight := 30;
  Dog.FurColor := 'Blond';
  Animal := Dog;
  TgoBsonSerializer.Serialize(Animal, Json); // Result:
  // { "_t" : "TDog", "Weight" : 30.0, "FurColor" : "Blond" }

  TgoBsonSerializer.Deserialize(Json, Rehydrated);
  // This will actually create a TDog instance (instead of TAnimal)
end;

In this case, an extra "_t" element is added (called a Discriminator) that specifies the actual type that is serialized. This way, when you deserialize a TAnimal, and the JSON/BSON contains a discriminator, it knows what actual type of class to instantiate.

However, this only works if the serialization engine “knows” about the TDog type. You have to let the engine know what kind of sub classes can be expected when deserializing. You do this by calling RegisterSubClass(es):

TgoBsonSerializer.RegisterSubClass(TDog);

Note that this is only needed if you plan to deserialize dogs using type TAnimal. If you always serialize and deserialize dogs as TDog, then you don’t need to do this.

You can choose to always serialize a discriminator, even if not strictly necessary, by adding a BsonDiscriminator attribute to the class:

type
  [BsonDiscriminator(True)]
  TAnimal = class
  public
    Weight: Double;
  end;

The True argument indicates that the discriminator is required. You can also specify a custom discriminator name using the same attribute:

type
  [BsonDiscriminator('animal', True)]
  TAnimal = class
  public
    Weight: Double;
  end;

This will serialize the discriminator as { "_t" : "animal" } instead of using the Delphi type name { "_t" : "TAnimal" }. In this case, the second parameter (True) is optional. If not specified, the discriminator is not required.

Custom Serialization

Is some situations, you may want to customize the way a certain type is (de)serialized entirely. For example, take the TRect type from the System.Types unit. This record has a variant part:

type
  TRect = record
    ...
  case Integer of
    0: (Left, Top, Right, Bottom: FixedInt);
    1: (TopLeft, BottomRight: TPoint);
  end;

The serialization engine will serialize all these fields:

Rect := TRect.Create(1, 2, 3, 4);
TgrBsonSerializer.Serialize(Rect, Json);

The result is very redundant:

{ "Left" : 1,
  "Top" : 2,
  "Right" : 3,
  "Bottom" : 4,
  "TopLeft" : { "X" : 1, "Y" : 2 },
  "BottomRight" : { "X" : 3, "Y" : 4 } }

It would be much more efficient to serialize only the first 4 fields, or even better yet, to serialize a TRect as a simple array of 4 integers. To do this, you have to create and register a custom serializer for this type.

First, you create a class derived from TgoBsonSerializer.TCustomSerializer. You only need to override its Serialize and Deserialize methods. In those methods you perform the type-specific (de)serialization. Both methods have an untyped AValue parameter that you must cast to the actual type (TRect in this example):

type
  TRectSerializer = class(TgoBsonSerializer.TCustomSerializer)
  public
    procedure Serialize(const AValue; const AWriter: IgoBsonBaseWriter); override;
    procedure Deserialize(const AReader: IgoBsonBaseReader; out AValue); override;
  end;

procedure TRectSerializer.Deserialize(const AReader: IgoBsonBaseReader;
  out AValue);
var
  Rect: TRect absolute AValue;
begin
  AReader.ReadStartArray;
  Rect.Left := AReader.ReadInt32;
  Rect.Top := AReader.ReadInt32;
  Rect.Right := AReader.ReadInt32;
  Rect.Bottom := AReader.ReadInt32;
  AReader.ReadEndArray;
end;

procedure TRectSerializer.Serialize(const AValue;
  const AWriter: IgoBsonBaseWriter);
var
  Rect: TRect absolute AValue;
begin
  AWriter.WriteStartArray;
  AWriter.WriteInt32(Rect.Left);
  AWriter.WriteInt32(Rect.Top);
  AWriter.WriteInt32(Rect.Right);
  AWriter.WriteInt32(Rect.Bottom);
  AWriter.WriteEndArray;
end;

Here, the absolute keyword is used to typecast the untyped AValue parameter to its actual type.

Next, you need to register the custom serializer for the type. For our example:

TgoBsonSerializer.RegisterCustomSerializer<TRect>(TRectSerializer);

Now, when you serialize the TRect example from earlier, the result is just:

[1, 2, 3, 4]

Note that custom serializers currently only work for record types.

Notes

  • The Serialize and Deserialize methods will raise an exception if the type is not serializable, or if the JSON/BSON to deserialize is invalid. To prevent exceptions, you can use the TrySerialize and TryDeserialize methods instead. These return False if (de)serialization failed.
  • Members of type TDateTime are expected to be in UTC format. No attempt is made to convert from local time to UTC and vice versa.

For more information, consult the API documentation for the Serialization API level.

Coming up…

Hope you are still with me. We covered a lot of ground here. JSON is an important data exchange format nowadays, so it warrants some attention. We use JSON and BSON in a lot of places in our app and BAAS. From REST calls to storing application settings to data storage in NoSQL databases.

In a future post we will present our own MongoDB database engine, which heavily depends on this JSON/BSON library.

13 thoughts on “Efficient and easy-to-use JSON and BSON library

    1. Nice catch, yes that should more clearly say, our own MongoDB database “driver” written in Delphi, and yes it is much more efficient and performs better that other implementations.

      Like

  1. […] am going to do the same thing using the Grijjy library (that has serialization/deserialization and BSON support as a […] .

    Like

  2. Hello, how is everything?

    I found a problem in record serialization when using property no fields are serialized please see example.

    type
    TOrderDetail = record
    public
    Product: String;
    Quantity: Integer;
    end;
    TOrderDetails = TArray;

    TOrder = record
    private
    FCustomer: String;
    FOrderDetails: TOrderDetails;
    procedure SetOrderDetails( const Value: TOrderDetails );
    public
    property Customer: String read FCustomer write FCustomer;
    property OrderDetails: TOrderDetails read FOrderDetails write SetOrderDetails;
    end;

    procedure TForm1.Button1Click( Sender: TObject );
    var
    OrderB: TOrder;
    Json: string;
    begin
    TgoBsonSerializer.Serialize( Order, Json );
    ShowMessage( Json );
    end;

    Can you help me with this?
    Thanks

    Like

Leave a comment