Direct Mapping
Target = Source
Example:
Applicant.Age = Applicants.App1.Age
Multiple Mapping Definitions
Multiple mapping definitions can be specified, one per line, e.g.:
Applicant.Age = Applicant.Age
Applicant.PostCode = Applicant.Address.PostCode
Using this definition against the source JSON:
{
Applicant: {
Age: 23,
Address: {
HouseNameNumber: "Lime House",
Postcode: "AB12 3CD"
}
}
}
Would produce the following target JSON:
{
Applicant: {
Age: 23,
PostCode: "AB12 3CD"
}
}
Mapping definitions are applied in order so if we had the following mapping the application of the second line would overwrite the first, ending up with the target Address property containing the source HouseNameNumber value.
Applicant.Address = Applicant.Address.PostCode
Applicant.Address = Applicant.Address.HouseNameNumber
Source From or Target To Root
We may want to map the entire source object onto a property in the target (or map onto the entire target). This is done by leaving the source or target empty (since the path to root is empty).
For example:
SearchResult =
Would map the entire source object into the SearchResult property in the target object.= Applicant
Would map the Applicant property of the source onto the entire target.=
Would map the entire source onto the entire target.
Special Characters
Looking at a target or source definition we can see that each is a dot-separated list of path segments or nodes. e.g. in the definition, Applicant.Address
there are two segments, Applicant
and Address
.
A segment can be surrounded in double quotes and must be if it contains special characters, e.g. Applicant.Address."First Name"
.
Special characters are any that are not the following:
- Alphanumeric
- _
- -
If a segment needs to contain a double quote then use a second double quote as an escape character, e.g. "My ""Fun"" Name"
would be the correctly quoted and escaped representation of the segment name My "Fun" Name
.
Variables
Variables are used to hold simple data (e.g. a string value or value that can be simply converted to a string) from a source JSON object. The variables are then used in the target definition in place of a hard-coded string.
Variables are always of the form $(name)
where name
is a valid variable name.
A variable name must only contain alphanumeric characters or the following special characters:
- _
- -
Property Capture
A variable can be used to capture all property names on a source object and then use those property names on the target object.
So the following mapping:
Addresses[$(applicantId)] = Applicants[$(applicantId)].Address
Given the following source object:
{
Applicants: {
App1: {
Age: 23,
Address: {
HouseNameNumber: "Lime House",
Postcode: "AB12 3CD"
}
},
App2: {
Age: 23,
Address: {
HouseNameNumber: "23",
Postcode: "WX89 0YZ"
}
}
}
}
Would produce the following target object:
{
Addresses: {
App1: {
HouseNameNumber: "Lime House",
Postcode: "AB12 3CD"
},
App2: {
HouseNameNumber: "23",
Postcode: "WX89 0YZ"
}
}
}
Filtering Property Capture
Variable values assigned through property capture can be filtered to restrict the set of values they capture.
Filters must use the following rules:
- The most basic filter is defined as a string value that matches a value that could be assigned to the variable and acts to include only that value
- Can be combined using
&&
and||
(for 'and' and 'or') - Can be negated using
!
- Can be grouped / nested using brackets
- Can use basic pattern matching using the star (
*
) symbol as a wild-card - If a filter contains whitespace or any special characters (other than a star - this is always treated as a wild-card) then it must be wrapped in double quotes
Examples:
App1
Only include the value App1 (assuming it exists otherwise no values will be included)!App1
Include any value that is not App1!App1 && !App2
Include anything except App1 or App2App*
Include any values that start with App(App* && !App1) || ABC
Include the value ABC and any values that start with App except App1!"Frank Jones"
Include any value that is not 'Frank Jones'
Filters are applied to variables by specifying them inside curly brackets directly after the variable name in the source specification. In the following example a filter to exclude App1 is being applied to the applicantId variable:
Addresses[$(applicantId)] = Applicants[$(applicantId){!App1}].Address
Property Value Capture
By default property capture will assign the properties name to the variable, it is also possible to use the properties value (or some sub-value).
This is done by specifying following the variable with a path specification in brackets.
A path specification consists of a colon followed by the path to the desired property. A colon with no path indicates that the current properties value should be used.
So using the following mapping:
Applicants[$(applicantId)] = Applicants[$(applicantId)(:Id)]
With the following source JSON:
{
Applicants: {
A: {
Id: 'App1',
Age: 23
},
B: {
Id: 'App2',
Age: 27
}
}
}
Would produce the following target JSON:
{
Applicants: {
App1: {
Id: 'App1',
Age: 23
},
App2: {
Id: 'App2',
Age: 27
}
}
}
This approach can also be used with source arrays so using the above mapping, the following source object would produce the same target object as above.
{
Applicants: [
{
Id: 'App1',
Age: 23
},
{
Id: 'App2',
Age: 27
}
]
}
If using value capture and filtering, the value capture is specified first, e.g. Applicants[$(applicantId)(:Id){!App1}]
.
Multi-Capture
It is possible to capture multiple variables at the same position if required. To do this simply comma separate the variable definitions.
In the following example we are capturing the applicants Id using property name capture and the applicants age using property value capture.
ApplicantsByAge[$(applicantAge)][$(applicantId)] = Applicants[$(applicantId),$(applicantAge)(:Age)].Name
Using the following source JSON:
{
Applicants: {
App1: {
Age: 23,
Name: "Frank"
},
App2: {
Age: 25,
Name: "Joe"
},
App3: {
Age: 23,
Name: "Bob"
}
}
}
Would produce the following target JSON:
{
ApplicantsByAge: {
23: {
App1: "Frank",
App3: "Bob"
},
25: {
App2: "Joe"
}
}
}
When using multi-capture with filters, the filters are still specified and applied per variable, if a property is filtered out on one of the filters then that applies across the board.
ApplicantsByAge[$(applicantAge)][$(applicantId)] = Applicants[$(applicantId){!App1},$(applicantAge)(:Age){!25}].Name
Using the above mapping with the previous example source JSON would produce the following output JSON:
{
ApplicantsByAge: {
23: {
App3: "Bob"
}
}
}
Combining Variables for Property Names
We may need to capture multiple values and combine them to produce the property name in the target JSON. Target property names are actually interpolated strings with variables being replaced so we could have the following specification:
Applicants[$(applicantFirstName)_$(applicantLastName)] = Applicants[$(applicantFirstName)(:FirstName),$(applicantLastName)(:LastName)].Age
Using the following source JSON
{
Applicants: [
{
FirstName: "Frank",
LastName: "Jones",
Age: 25
},
{
FirstName: "Billy",
LastName: "Baker",
Age: 34
}
]
}
Would produce the following target JSON:
{
Applicants: {
Frank_Jones: 25,
Billy_Baker: 34
}
}
Target Property Values
So far we have seen how to use variables to define target property names, we can also use variables to set target property values.
This is done by defining an assignment inside curly brackets at the most nested level possible with the assignment property being relative to the definition location. In the following example we are assigning the applicantAge variable to the Age property:
Applicants[$(applicantId)]{Age:$(applicantAge)}.Name = ApplicantsByAge[$(applicantAge)][$(applicantId)]
This could also be written as follows however this is more complex (hence we always want to specify assignment at the most nested level possible).
Applicants{$(applicantId).Age:$(applicantAge)}[$(applicantId)].Name = ApplicantsByAge[$(applicantAge)][$(applicantId)]
Using the following source JSON with the above definition:
{
ApplicantsByAge: {
23: {
App1: "Frank",
App3: "Bob"
},
25: {
App2: "Joe"
}
}
}
Would produce the following target JSON:
{
Applicants: {
App1: {
Age: 23,
Name: "Frank"
},
App2: {
Age: 25,
Name: "Joe"
},
App3: {
Age: 23,
Name: "Bob"
}
}
}
Targeting Property Values in Arrays
We can also target property values in arrays so we could have the following mapping definition:
Applicants[]{Age:$(applicantAge),Id:$(applicantId)}.Name = ApplicantsByAge[$(applicantAge)][$(applicantId)]
Note the use of the empty square brackets denoting an array and the comma separation for the multiple assignments
Given the following source JSON:
{
ApplicantsByAge: {
23: {
App1: "Frank",
App3: "Bob"
},
25: {
App2: "Joe"
}
}
}
This would produce the following target JSON:
{
Applicants: [
{
Age: 23,
Id: App1,
Name: "Frank"
},
{
Age: 23,
Id: App3,
Name: "Bob"
},
{
Age: 25,
Id: App2,
Name: "Joe"
}
]
}
More on Target Arrays
Any node in the target can be declared as an array, this is done as above by using the open-close square brackets (i.e. []
). The open-close square brackets should come immediately after the node name definition before any modifiers (e.g. before any target property setters).
The array declaration can be used on a plain node name or on an indexed node, e.g. both the following are valid:
Names[] = Applicants[$(applicantId)].Name
ChildNames.[$(applicantId)][] = Applicants[$(applicantId)].Children[$(childId)].Name
Each matching source element will be added to the target array as a new element. This is true even if mapping onto an existing target (see below for more details), no attempt is made to match existing array elements.
Partials
If our mapping starts getting complicated and has repetitive parts we may want to define partial mappings that we can reuse. This is done using partials. A partial is defined using double angle brackets and follows the same naming rules as variables. Partials can be used in either the source or target sides of the definition and can include variable names.
Assignment to a partial is done using the double colon operator, the partial can then be used in place of the full definition.
The following mapping declares two partials and then uses them in multiple individual mapping definitions.
<<sourceApplicant>>:: Application.Main.Applicants[$(applicantId)].Details
<<targetApplicant>>:: Applicants[$(applicantId)]
<<targetApplicant>>.Name = <<sourceApplicant>>.Name
<<targetApplicant>>.PostCode = <<sourceApplicant>>.Address.PostCode
<<targetApplicant>>.Title = <<sourceApplicant>>.Title.Name
Under the Hood
When actually applying a mapping the following steps are taken:
- Apply the source side of the mapping to the source JSON
- This will produce a set of individual values, each value with an associated set of variable values
- In the case of
Applicants[$(applicantId)].Name
this would give a set ofName
values, each with an associated value forapplicantId
- Apply the target side of the mapping to each value in the set of found values to produce the output JSON for that specific value, substituting the associated variable values as appropriate
- Combine all the output results to produce the final result
Mapping to an Existing Object
So far we have only considered mapping from a source object to a new (empty) target object. If we want to be able to map new data into an existing object there are a few more considerations.
The most basic approach would be to map into an empty object and then simply union the existing and new objects, taking the values from the new object where there are conflicts. This is often not desired though, for instance we may not want to map some properties that don't already exist in the target object or we may have a situation where multiple objects exist in the target that we want to map the same source value to.
The set of considerations we will handle are:
- Conditionally mapping based on existence of the target property
- Either map if existing or map if not existing
- Also supporting this based on existence of parent objects (e.g. map the name property if the applicant that it belongs to exists regardless of whether the name property exists on that applicant)
For now we will not support mapping a source value to multiple matching target values, this will simply cause an error in the mapping.
Conditional Mapping
Given the above requirements for conditional mapping we need a way to specify one of the following three requirements for a target path segment:
- Always map (whether it exists or not)
- Map only if the element exists
- Map only if the element does not exist
Always map is the default behavior so the syntax used above will all use the 'always map' behavior. i.e. The following would always map Name
regardless of whether that property exists in the target JSON.
Applicants[$(applicantId)].Name = Applicants[$(applicantId)(:Id)].Name
The other two behaviors require some additional syntax to apply a modifier to a path segment specifying the behavior. For this the modifier will be applied at the end of the segment it applies to (if the segment is quoted it is applied outside the quotes). The modifiers are as follows:
?
Map if the element does exist!
Map if the element does not exist
In the following example the name will be mapped only if the applicant with the specific Id exists in the target JSON (but we don't care whether the Name itself exists on the applicant).
Applicants[$(applicantId)]?.Name = Applicants[$(applicantId)(:Id)].Name
We can also combine these operators in a single mapping if we want. In the following example the Name will only be mapped if the applicant exists in the target JSON and does not already have a Name set.
Applicants[$(applicantId)]?.Name! = Applicants[$(applicantId)(:Id)].Name
Whilst any combination of operators like this is valid the only useful combination is as shown above with a ?
element and then further along an !
element. If we have !...?
then no mapping will ever happen since the requirements represent an impossibility. If we have multiple of the same operator then we only actually need the last one.
Restricting by Schema
For all the above mapping we could define a schema for the source or target objects (out of scope of this document but we can think about TypeScript interface definitions for POJO objects). We could then use those schema(s) to restrict what gets copied during mapping by ignoring anything that does not match them.
Imagine we had the following target schema definition (using TypeScript syntax):
interface ITarget {
Applicants: {
[id: string]: {
Age: number
}
}
}
If we were to use that when applying the following mapping:
Applicants[$(applicantId)] = Applicants[$(applicantId)(:Id)]
With the following source JSON:
{
Applicants: {
A: {
Id: 'App1',
Age: 23
},
B: {
Id: 'App2',
Age: 27
}
}
}
It would produce the following target JSON (note that the Id properties have been stripped out leaving only the Age properties in line with the target definition):
{
Applicants: {
App1: {
Age: 23
},
App2: {
Age: 27
}
}
}
Syntax Notes
Comments
Comments can be specified in the C single line style, prefixed with //
. A comment runs from the starting //
to the end of the line.
Blank Lines
Blank lines are allowed and simply ignored.
Splitting Lines
In all the examples we show each mapping definition on a single line. They can be split onto multiple lines by indenting. Any line that starts with some whitespace (and is not a comment) is considered a continuation of the previous line.
So the following:
Applicant.Name = Applicant.Name
Applicant.PostCode = Applicant.Address.PostCode
Could be written as follows and mean the same thing:
// Map the applicants name
Applicant.Name = Applicant.Name
// Map the postcode directly onto the applicant
Applicant.PostCode
= Applicant
.Address
// This is a comment about the postcode field
.PostCode