Variant object documents
With _Obj(), an object variant instance
will be initialized with data supplied two by two, as Name,Value
pairs, e.g.
var V1,V2: variant; // stored as any variant ... V1 := _Obj(['name','John','year',1972]); V2 := _Obj(['name','John','doc',_Obj(['one',1,'two',2.5])]); // with nested objects
Then you can convert those objects into JSON, by two means:
- Using the
VariantSaveJson()function, which return directly one UTF-8 content; - Or by trans-typing the
variantinstance into a string (this will be slower, but is possible).
writeln(VariantSaveJson(V1)); // explicit conversion into RawUTF8
writeln(V1); // implicit conversion from variant into string
// both commands will write '{"name":"john","year":1982}'
writeln(VariantSaveJson(V2)); // explicit conversion into RawUTF8
writeln(V2); // implicit conversion from variant into string
// both commands will write '{"name":"john","doc":{"one":1,"two":2.5}}'
As a consequence, the Delphi IDE debugger is able to display such variant
values as their JSON representation.
That is, V1 will be displayed as
'"name":"john","year":1982' in the IDE debugger Watch
List window, or in the Evaluate/Modify (F7) expression
tool.
This is pretty convenient, and much more user friendly than any class-based
solution (which requires the installation of a specific design-time package in
the IDE).
You can access to the object properties via late-binding, with any depth of nesting objects, in your code:
writeln('name=',V1.name,' year=',V1.year);
// will write 'name=John year=1972'
writeln('name=',V2.name,' doc.one=',V2.doc.one,' doc.two=',doc.two);
// will write 'name=John doc.one=1 doc.two=2.5
V1.name := 'Mark'; // overwrite a property value
writeln(V1.name); // will write 'Mark'
V1.age := 12; // add a property to the object
writeln(V1.age); // will write '12'
Note that the property names will be evaluated at runtime only, not at
compile time.
For instance, if you write V1.nome instead of
V1.name, there will be no error at compilation, but an
EDocVariant exception will be raised at execution (unless you set
the dvoReturnNullForUnknownProperty option to
_Obj/_Arr/_Json/_JsonFmt which will return a null
variant for such undefined properties).
In addition to the property names, some pseudo-methods are available for
such object variant instances:
writeln(V1._Count); // will write 3 i.e. the number of name/value pairs in the object document
writeln(V1._Kind); // will write 1 i.e. ord(sdkObject)
for i := 0 to V2._Count-1 do
writeln(V2.Name(i),'=',V2.Value(i));
// will write in the console:
// name=John
// doc={"one":1,"two":2.5}
// age=12
if V1.Exists('year') then
writeln(V1.year);
You may also trans-type your variant instance into a
TDocVariantData record, and access directly to its
internals.
For instance:
TDocVariantData(V1).AddValue('comment','Nice guy');
with TDocVariantData(V1) do // direct transtyping
if Kind=sdkObject then // direct access to the TDocVariantDataKind field
for i := 0 to Count-1 do // direct access to the Count: integer field
writeln(Names[i],'=',Values[i]); // direct access to the internal storage arrays
By definition, trans-typing via a TDocVariantData record is
slightly faster than using late-binding.
But you must ensure that the variant instance is really a
TDocVariant kind of data before transtyping e.g. by calling
DocVariantType.IsOfType(aVariant).
Variant array documents
With _Arr(), an array variant instance
will be initialized with data supplied as a list of Value1,Value2,...,
e.g.
var V1,V2: variant; // stored as any variant ... V1 := _Arr(['John','Mark','Luke']); V2 := _Obj(['name','John','array',_Arr(['one','two',2.5])]); // as nested array
Then you can convert those objects into JSON, by two means:
- Using the
VariantSaveJson()function, which return directly one UTF-8 content; - Or by trans-typing the
variantinstance into a string (this will be slower, but is possible).
writeln(VariantSaveJson(V1));
writeln(V1); // implicit conversion from variant into string
// both commands will write '["John","Mark","Luke"]'
writeln(VariantSaveJson(V2));
writeln(V2); // implicit conversion from variant into string
// both commands will write '{"name":"john","array":["one","two",2.5]}'
As a with any object document, the Delphi IDE debugger is able to
display such array variant values as their JSON
representation.
Late-binding is also available, with a special set of pseudo-methods:
writeln(V1._Count); // will write 3 i.e. the number of items in the array document
writeln(V1._Kind); // will write 2 i.e. ord(sdkArray)
for i := 0 to V1._Count-1 do
writeln(V1.Value(i),':',V2._(i));
// will write in the console:
// John John
// Mark Mark
// Luke Luke
if V1.Exists('John') then
writeln('John found in array');
Of course, trans-typing into a TDocVariantData record is
possible, and will be slightly faster than using late-binding.
Create variant object or array documents from JSON
With _Json() or _JsonFmt(), either a
document or array variant instance will be
initialized with data supplied as JSON, e.g.
var V1,V2,V3,V4: variant; // stored as any variant
...
V1 := _Json('{"name":"john","year":1982}'); // strict JSON syntax
V2 := _Json('{name:"john",year:1982}'); // with MongoDB extended syntax for names
V3 := _Json('{"name":?,"year":?}',[],['john',1982]);
V4 := _JsonFmt('{%:?,%:?}',['name','year'],['john',1982]);
writeln(VariantSaveJSON(V1));
writeln(VariantSaveJSON(V2));
writeln(VariantSaveJSON(V3));
// all commands will write '{"name":"john","year":1982}'
Of course, you can nest objects or arrays as parameters to the
_JsonFmt() function.
The supplied JSON can be either in strict JSON syntax, or with the
MongoDB extended syntax, i.e. with unquoted property names.
It could be pretty convenient and also less error-prone when typing in the
Delphi code to forget about quotes around the property names of your JSON.
Note that TDocVariant implements an open interface for adding any
custom extensions to JSON: for instance, if the SynMongoDB.pas
unit is defined in your application, you will be able to create any MongoDB
specific types in your JSON, like ObjectID(), new
Date() or even /regex/option.
As a with any object or array document, the Delphi IDE
debugger is able to display such variant values as their JSON
representation.
Per-value or per-reference
By default, the variant instance created by _Obj() _Arr()
_Json() _JsonFmt() will use a copy-by-value pattern.
It means that when an instance is affected to another variable, a new
variant document will be created, and all internal values will be
copied. Just like a record type.
This will imply that if you modify any item of the copied variable, it won't change the original variable:
var V1,V2: variant; ... V1 := _Obj(['name','John','year',1972]); V2 := V1; // create a new variant, and copy all values V2.name := 'James'; // modifies V2.name, but not V1.name writeln(V1.name,' and ',V2.name); // will write 'John and James'
As a result, your code will be perfectly safe to work with, since
V1 and V2 will be uncoupled.
But one drawback is that passing such a value may be pretty slow, for instance, when you nest objects:
var V1,V2: variant; ... V1 := _Obj(['name','John','year',1972]); V2 := _Arr(['John','Mark','Luke']); V1.names := V2; // here the whole V2 array will be re-allocated into V1.names
Such a behavior could be pretty time and resource consuming, in case of a huge document.
All _Obj() _Arr() _Json() _JsonFmt() functions have an optional
TDocVariantOptions parameter, which allows to change the behavior
of the created TDocVariant instance, especially setting
dvoValueCopiedByReference.
This particular option will set the copy-by-reference pattern:
var V1,V2: variant; ... V1 := _Obj(['name','John','year',1972],[dvoValueCopiedByReference]); V2 := V1; // creates a reference to the V1 instance V2.name := 'James'; // modifies V2.name, but also V1.name writeln(V1.name,' and ',V2.name); // will write 'James and James'
You may think this behavior is somewhat weird for a variant
type. But if you forget about per-value objects and consider those
TDocVariant types as a Delphi class instance (which
is a per-reference type), without the need of having a fixed schema
nor handling manually the memory, it will probably start to make sense.
Note that a set of global functions have been defined, which allows direct
creation of documents with per-reference instance lifetime, named
_ObjFast() _ArrFast() _JsonFast() _JsonFmtFast().
Those are just wrappers around the corresponding _Obj() _Arr() _Json()
_JsonFmt() functions, with the following JSON_OPTIONS[true]
constant passed as options parameter:
const
/// some convenient TDocVariant options
// - JSON_OPTIONS[false] is _Json() and _JsonFmt() functions default
// - JSON_OPTIONS[true] are used by _JsonFast() and _JsonFastFmt() functions
JSON_OPTIONS: array[Boolean] of TDocVariantOptions = (
[dvoReturnNullForUnknownProperty],
[dvoReturnNullForUnknownProperty,dvoValueCopiedByReference]);
When working with complex documents, e.g. with BSON / MongoDB documents, almost all content will be created in "fast" per-reference mode.
Advanced TDocVariant process
Object or array document creation options
As stated above, a TDocVariantOptions parameter enables to
define the behavior of a TDocVariant custom type for a given
instance.
Please refer to the documentation of this set of options to find out the
available settings. Some are related to the memory model, other to
case-sensitivity of the property names, other to the behavior expected in case
of non-existing property, and so on...
Note that this setting is local to the given variant
instance.
In fact, TDocVariant does not force you to stick to one memory
model nor a set of global options, but you can use the best pattern depending
on your exact process.
You can even mix the options - i.e. including some objects as
properties in an object created with other options - but in this case, the
initial options of the nested object will remain. So you should better use this
feature with caution.
You can use the _Unique() global function to force a variant
instance to have an unique set of options, and all nested documents to become
by-value, or _UniqueFast() for all nested documents to
become by-reference.
// assuming V1='{"name":"James","year":1972}' created by-reference
_Unique(V1); // change options of V1 to be by-value
V2 := V1; // creates a full copy of the V1 instance
V2.name := 'John'; // modifies V2.name, but not V1.name
writeln(V1.name); // write 'James'
writeln(V2.name); // write 'John'
V1 := _Arr(['root',V2]); // created as by-value by default, as V2 was
writeln(V1._Count); // write 2
_UniqueFast(V1); // change options of V1 to be by-reference
V2 := V1;
V1._(1).name := 'Jim';
writeln(V1);
writeln(V2);
// both commands will write '["root",{"name":"Jim","year":1972}]'
The easiest is to stick to one set of options in your code, i.e.:
- Either using the
_*()global functions if your business code does send someTDocVariantinstances to any other part of your logic, for further storage: in this case, the by-value pattern does make sense; - Or using the
_*Fast()global functions if theTDocVariantinstances are local to a small part of your code, e.g. used as schema-less Data Transfer Objects (DTO).
In all cases, be aware that, like any class type, the
const, var and out specifiers of method
parameters does not behave to the TDocVariant value, but to its
reference.
Integration with other mORMot units
In fact, whenever a schema-less storage structure is needed, you
may use a TDocVariant instance instead of class or
record strong-typed types:
- Client-Server ORM will support
TDocVariantin any of theTSQLRecord variantpublished properties; - Interface-based services will support
TDocVariantasvariantparameters of any method, which make them as perfect DTO; - Since JSON support is implemented with any
TDocVariantvalue from the ground up, it makes a perfect fit for working with AJAX clients, in a script-like approach; - If you use our
SynMongoDB.pasunit to access a MongoDB server,TDocVariantwill be the native storage to create or access BSON arrays or objects documents; - Cross-cutting features (like logging or
record/ dynamic array enhancements) will also benefit from thisTDocVariantcustom type.
We are pretty convinced that when you will start playing with
TDocVariant, you won't be able to live without it any more.
It introduces the full power of late-binding and schema-less patterns to your
application, which can be pretty useful for prototyping or in Agile
development.
You do not need to use scripting engines like Python or JavaScript to have this
feature, if you need it.
Feedback and comments are welcome in our forum, as usual!

