Data Model
Functionality in AOP is defined independently of concrete programming languages. The same applies for data structures. However, all Data Objects in AOP are structured according to the following scheme:

Value is an arbitrary piece of data, and can be either be a simple value or an Object, Map, Array or Indexed Array. With nested values, principally any Data Object, no matter how complex, can be described.
Property is a “named value” consisting of a Property name (the identifier of the Property, of type Symbol) and a value (which, of course, may also be an Object, Map or Array). Whenever Properties are mentioned, a set of such Properties is meant.
Object is defined as a set of Properties, or in other words, an associative container that maps Property names to values. All Properties have to be unique, i.e. no Property name exists more than once within an Object. When we refer to an Object type, we mean the set of concrete Properties that can exist for an Object of this type.
Map is an associative container mapping arbitrary unicode strings to values. Conceptually, while Maps are similar to Objects, Maps are usually not constrained to a set of specific keys, while Objects usually are.
Array is a sequential collection of values.
Indexed Array is an associative container, mapping non-negative 63-bit integer keys (or, any non-negative values of a 64-bit signed integer) to values. It is functionally a sparse array.
💡Data Objects in AOP are transmitted as JSON. Even if the schemas look similar, there is no one-to-one correspondence between an AOP value and its representation in JSON; for example, an object in JSON is not the same as an Object in AOP. The types in AOP should be considered logical types and not determined by their transport method.
Value types
Here is a list of the value types in AOP, not all of which have a direct JSON representation.
| Type | JSON | Description |
|---|---|---|
| Simple types: | ||
| (null) | null | Empty value, not a type specifier. |
| bool | true / false | Boolean |
| int | number | 64-bit signed integer |
| float | number* | 64-bit float (IEEE 754 double-precision floating-point format) * In JSON, the float values NaN and positive/negative Infinity are transmitted as strings according to special values. |
| string | string | Unicode string |
| guid | string | 128bit number; see below for details |
| time | string | Time as ISO 8601 compatible subset; see below for details |
| data | string | Base64-coded as per RFC 4648 §4 |
| Other types: | ||
| value | Generic value type specifier. | |
| object | object | Generic object. As object type specifier usually concrete object types are used. |
| <Type>{} | object | Map - the entries are identified via string ids. If declared as value{}, this is a generic value map that can store any AOP value. |
| <Type>[] | array | Array, addressed via consecutive index, up to a maximum size of 2^63. |
| <Type>[#] | object | Indexed Array - The entries of this array are also addressed via an index between 0 and 2^63-1; the indices are not contiguous. |
These value type specifiers are used all over AOP, e.g., in XML Definitions. For <Type> any of the types (bool, int, float, string, guid, date, time, data, object) can be used.
Time format
AOP’s time format is a subset of ISO 8601. The precision is never less than one second. Timezones (or rather, timezone offsets) are never used and instead all transmitted dates and times are always in UTC. The format string is YYYY-MM-DDThh:mm:ss.sssZ; an example is “2022-10-20T13:30:31.233Z”. The sub-second component .sss can be of any length up to 7 or omitted entirely, so “2022-10-20T13:30:31Z” and “2022-10-20T01:31:41.592653Z” are both valid. Implementations receiving a date with sub-second components are not required to store that time with more than second-precision.
For the purposes of AOP value conversions, an empty string is convertible to a specific implementation’s epoch time.
See RFC 3339 for additional details on the ISO 8601 format.
GUID format
AOP GUIDs are like GUIDs in Microsoft Windows, which in turn are mostly implemented as specified in RFC4122. It should be possible with relatively little effort to use any RFC4122-compliant UUID implementation.
The binary representation consists of a 4-byte integer, two 2-byte integers, and one array of 8 bytes, totaling 16 bytes. The integers are stored in little-endian format (unlike standard RFC4122, which is big endian).
The text representation is in so-called “8-4-4-4-12 format”, with curly braces. Each byte is printed as two hexadecimal characters. The 16 bytes are grouped into 5 groups, with the first group containing the first 4 bytes (the first integer) in big-endian order, the second and third group containing the next two integers (2 bytes each) in big-endian order, and the fourth and fifth group containing the array of 8 bytes (4 bytes and 6 bytes respectively) in the order they appear in the binary representation. The groups are concatenated with ‘-’. The hexadecimal values ‘a’ through ‘f’ are output as lowercase characters and are case-insensitive on input. On output, the entire string is surrounded with curly braces. On input, the curly braces are optional but recommended. For the purposes of AOP value conversions, an empty string is convertible to a null GUID.
As an example, these representations are equivalent:
- Text format:
{00112233-4455-6677-8899-aabbccddeeff} - Binary format (as hex octets):
33 22 11 00 55 44 77 66 88 99 aa bb cc dd ee ff
GUID comparisons
When ordering two GUID values, the following rules apply:
- An empty GUID is less than any non-empty GUID.
- A GUID of the Microsoft variant (byte 8 begins with bits 110) and version 0 (upper 4 bits of byte 7 are zero) is an ID GUID. Two ID GUIDs are compared by comparing the first 4-byte integer of the GUID; the remaining bytes are not considered. ID GUIDs are less than any non-empty non-ID GUID.
- All other GUIDs are compared by comparing their binary representations lexicographically (i.e.,
memcmp).
The default comparison of GUID implementations in some standard libraries may differ from the above rules, so caution is advised.
Conversions
Values are not necessarily transmitted via JSON in such a way that an equivalent AOP type arrives on the other side; or in other words, the actual AOP type received is sometimes only indirectly apparent. Therefore the interpretation of a value is always the responsibility of the user. If required, types are converted automatically.
The following tables show which conversions are implemented by default. Conversions that are not possible are marked with ❌.
Scalar types
| ↓from|to→ | bool | int | float | string | guid | time | data |
|---|---|---|---|---|---|---|---|
| bool | = | true → 1 false → 0 |
true → 1.0 false → 0.0 |
true → “1” false → “0” |
❌ | ❌ | ❌ |
| int | 1 → true 0 → false else ❌ |
= | =3 | decimal-int |
❌ | ❌ | ❌ |
| float | 1.0 → true 0.0 → false else ❌ |
round towards zero; if exceeds int64 range → ❌ |
= | float or special values4 |
❌ | ❌ | ❌ |
| string | “1”, “true”1 → true “0”, “false”1 → false else ❌ |
number-literal2,else ❌ |
number-literal2 or special values,else ❌ |
= | "{GUID}" or “GUID”, else ❌ |
ISO 8601 UTC, else ❌ |
base64 → data, else ❌ |
| guid | ❌ | ❌ | ❌ | "{GUID}" | = | ❌ | ❌ |
| time | ❌ | ❌ | ❌ | ISO 8601 UTC | ❌ | = | ❌ |
| data | ❌ | ❌ | ❌ | base64 | ❌ | ❌ | = |
1 Case-insensitive.
2 A number literal (int or float) is parsed from the string according to the number-literal grammar rule defined in the Expressions documentation.
If the parsed literal does not yet match the target type (e.g., parsed literal is a float but target type is int), the above conversion rules are then applied to the parsed literal.
3 Note that converting from int to float may suffer from a loss of precision.
Above the threshold of 9'007'199'254'740'992, which is 2^53, the double precision is not enough to represent an integer value precisely.
For example, the int 9'007'199'254'740'993 converted to a float is 9'007'199'254'740'992.0.
This is defined in the IEEE Standard for Floating-Point Arithmetic (IEEE 754).
4 Converting float to string produces a string that, when converted back to float, results in the same float value (round-trip).
The exact format of the produced string may vary by implementation, but always conforms to the float grammar rule or is one of the special float values.
Special float values
In addition to the float conversions defined above, the special float values NaN and positive/negative Infinity can be converted to and from the data type “string” according to the following table:
| float | string |
|---|---|
| NaN | “NaN” |
| Positive Infinity | “Infinity” |
| Negative Infinity | “-Infinity” |
These string equivalents are case-insensitive.
Container types
| ↓from|to→ | <Type>{} | <Type>[] | <Type>[#] | Object |
|---|---|---|---|---|
| <Type>{} | = | if all keys int and sequential | if all keys int | if all keys are valid Property names and distinct from each other (with respect to casing) |
| <Type>[] | keys stringified | = | indices transformed to keys | - |
| <Type>[#] | keys stringified | if keys sequential | = | - |
| object | = | - | - | = |
Conversion between containers of different value types (e.g., value{} to string{}, or int[] to float[]) succeeds if and only if each value can be successfully converted.
Comparison of values
When comparing two values of different types, the following promotion order applies:
- Promotion order of scalar types:
bool>float>int1 >guid>time>data>string. - Promotion order of container types:
value[]>value[#]>object>value{}.
The value of the “lower” type is converted to the “higher” type and then compared as if they were of the same type. If the conversion fails, the comparison also fails.
1 There is one exception: When comparing an int to a string, the string is first parsed as a number literal according to the number-literal grammar rule defined in the Expressions documentation or special float values, and if that parsing is successful, the resulting number (int or float) is compared to the int.
This is to prevent a straight conversion of string to int from truncating the decimal part of floats, which would lead to unexpected results.
Symbols
A Symbol is an identifier, defined to match the regular expression @?[A-Za-z][A-Za-z0-9_]*. Its base AOP type is string.
They are compared case-insensitively over the ASCII character range.
A symbol represents a human-readable identifier within AOP. For example, names of function parameters or IDs of Properties, Objects, States, and Events are all represented by symbols.
Symbols with the optional prefix @ are reserved for internal use.
Codes
A Code is a special identifier used by some AOP Types, such as Datapoints or Extensions.
The formal definition is as follows:
RegEx:
[A-Za-z0-9_\-\/:\#]+(?:\.[A-Za-z0-9_\-\/:\#]+)*
EBNF:
Code = 1*CodeChar *[ "." 1*CodeChar ]
CodeChar = ALPHA | DIGIT | "_" | "-" | "/" | ":" | "#"
Its base AOP type is string.
They are compared case-insensitively.
Symbol paths
A Symbol Path is a sequence of Symbols separated by dots (.). Its base AOP type is string.
They are compared case-insensitively over the ASCII character range.
A symbol path is conceptually similar to to Symbol but allows a hierarchical grouping of identifiers. For example, it is used for AOP method names, or for types of device adapter Objects.
An operation that checks if a symbol path is a prefix of another should take symbol boundaries into account. For example, “Foo”, “Foo.Bar” and “Foo.Bar.Baz” are prefixes of the symbol path “Foo.Bar.Baz”, but “F” or “Foo.B” are not.
An empty symbol path is not allowed unless documented otherwise.
Property paths
The data model allows forming complex Objects containing nested data structures, like Objects, containing Arrays, Maps or other Objects, which themselves contain Objects and so on. Not only root-level Properties, but any Property in the hierarchy of such Objects are directly addressable using Property Paths. Property Paths are used in, for example, the Set() methods of data services to update only specific aspects of an object.
A Property Path is built from components and evaluated from left to right, with each component iterating further down the root Object. Each path component is one instruction that can both be used to iterate into an Object or to help construct a partial Object from the path.
- An Object Property is referenced by specifying a
.and the Property name. The starting.must be left off for the first Property. - An Array is referenced by putting a zero-based integer index within square brackets, e.g.,
[5]. - A value map key is referenced by putting the key name in curly brackets, e.g.,
{MyKey}. This convention matches how value Maps are signified in AOP documentation (value{}). Curly brackets or backslashes are escaped with a prepended backslash, so something likeC:\Users\Dieter\{Something}is transformed into a path component like{C:\\Users\\Dieter\\\{Something\}}. Other symbols don’t need to be escaped. - An Indexed Array is referenced like an array, except that a
#is placed as the first character within the square brackets (e.g.,[#1337]). This convention also matches how they are signified in AOP documentation (value[#]).
Example: An Object “Person” contains the Properties “Name”, “Address” and “WishList”, where the address is an Object with the Properties “Street” and “City”, and “WishList” is an Array with Objects that have the Property “Wish” and “Priority”. The “City” can be addressed directly via “Address.City” as a Property, the second wish via “WishList[1].Wish”, etc.
An example Property Path that demonstrates all of the possible syntactical features:
myObject.subObject.anArray[12].anIndexedArrayContainingValueMaps[#17]{cheesy\{keys\}}[5].yetAnotherValueMap{WithAReallyBoringKey}
When iterating an Object given a path, the container equivalencies and conversions are be taken into account when indexing into the container; however, this does not necessarily mean that the entire container must be convertible. For example, if [5] is specified and the actual value is an Indexed Array, it acts as if [#5] was specified, even if [#4] does not exist and therefore a conversion of the entire container to an array would fail. Any potentially indeterministic cases (e.g. using .test to index into a map containing both test and Test keys) are undefined behavior.
Implementation guide
These are some general guidelines for AOP implementations that should be followed to make programming with AOP feel natural and transparent to anybody familiar with the implementation’s ecosystem.
Implementations should specify specific types in their ecosystem to represent each scalar AOP data type. If necessary, the language projection will provide such a type, if the ecosystem does not provide one (e.g., Typescript does not define a Guid class).
Implementations should offer a generic “AOP value” type, which can encapsulate any AOP type but is (mostly or wholly) opaque regarding its contents and instead offers methods to retrieve a value in a requested type. Those methods will perform necessary conversions and return a value of the requested type, if successful, or fail in a defined and idiomatic manner (for example, by returning T? in TypeScript, or using the TryGet-paradigm in C#).
Implementations should also define separate types for AOP Objects, value Maps, Arrays, and Indexed Arrays, which offer a view on a AopValue so long as the value can be represented as such a container. These should strive to act similarly to other corresponding containers in that specific environment, e.g., maps will implement IReadOnlyDictionary in C# or implement the AssociativeContainer Trait in C++.
Implementations should offer a dedicated helper type for Property Paths.