Skip to content

Scripting

Für komplexere Umwandlungsaufgaben hat Synesty Studio die Scriptsprache Freemarker integriert, die es erlaubt komplexere Formeln, Funktionen und Logik auszuführen. Damit hat man die Möglichkeit, ähnlich wie mit Funktionen aus Tabellenkalkulationsprogrammen durch Bedingungen (if/else), Schleifen oder Manipulation von Zeichenketten Einfluss auf die Werte zu nehmen. 

Folgende Funktionen (sog. Built-Ins oder Direktiven) werden am häufigsten benötigt:

Grundlagen

Im Mapper gibt es zwei Möglichkeiten einen Input Wert für eine Spalte festzulegen:

  • Quelle
  • Wert

Folgender Screenshot zeigt, wie ein Spaltenwert durch die Auswahlbox Quelle und einmal mit dem Value Feld gefüllt wird. 

Scripting zur Manipulation von Spalten

Scripting ist hauptsächlich im Wert-Feld, aber auch im Skript-Feld anwendbar und erfolgt durch die Eingabe von sogenannten Freemarker Ausdrücken. 
 

Folgende Beispiele können mit unserem Beispiel-Spreadsheet ausprobiert werden.

Beispiel

${name}
<#if name?contains("a")>
Die Spalte 'Name' beinhaltet ein A.
<#else>
Es kommt kein A in der Spalte A vor.
</#if>

Manipulationen von Spalten

String Manipulation

Freemarker besitzt viele sog. built-ins zur Manipulation von Strings, also Zeichenketten.

Folgender Ausdruck gibt unseren ${name!} in GROßBUCHSTABEN aus:

${name!?upper_case}

Das Fragezeichen ? wendet das  “built-in” upper_case auf die Variable name an.

Beispiel: Nur den Text zwischen den Tags <p> und </p> ausgeben:

${"Test 123 <p>test</p>"?keep_after('<p>')?keep_before('</p>')}
Ausgabe: test

Zahlen Manipulation und Rechnen (arithmetische Funktionen)

Freemarker besitzt built-ins zur Manipulation von Zahlen.

${price?number?string.currency}

Die Ausgabe ist $42.00 Die verwendete Locale ist die des Servers, d.h. en_US. Das ?number Built-In ist notwendig, um aus einem String erst eine Zahl zu machen. Wenn die entsprechende Variable bereits eine Zahl ist, is ?number nicht notwendig.  Um den Preis in deutscher Locale auszugeben wäre es

<#setting locale="de_DE">
${price?number?string.currency}

Die Ausgaben wäre bei de_DE 42,00 €

Rechnen

Mit Freemarker kann man auch rechnen. Lesen Sie dazu mehr über arithmetische Funktionen.

Beispiel:

${1 + 2}
# gibt 3 aus
${spalte1?number + spalte2?number}
# gibt die Summer von zwei Spalten aus.

Datumsfunktionen

Datumsmanipulation ist auch mit Freemarker möglich.

<#assign purchaseDate = .now>
${purchaseDate?string('yyyy-MM-dd HH:mm:ss zzzz')}

Ausgabe ist z.B. 2013-07-12 19:19:15 Central European Summer Time

Datumsangaben Vergleichen

Vergleich zweier Datumsangaben

<#if ("2020-12-11 20:09:42"?datetime('yyyy-MM-dd HH:mm:ss') > "2020-12-10 10:09:42"?datetime('yyyy-MM-dd HH:mm:ss')) >.
Das erste Datum ist neuer.
<#else>
Das erste Datum ist älter.
</#if>

Siehe dazu auch das Cookbook zu Datumsarithmetik / Rechnen mit Datumsangaben.

Bedingungen / Boolean expressions (IF/ELSE)

Wenn/Dann Logik wird durch sog. Boolesche Ausdrücke realisiert. Damit lassen sich Spalteninhalte in Abhängigkeit von anderen Quellspalten manipulieren.

Zum Beispiel könnte man sagen, dass die Versandkosten 0 EUR betragen sollen, wenn der Preis größer 100 EUR ist. Ansonsten könnten die Versandkosten 5 EUR betragen.  

Beispiel Versandkosten

<#if (price?number >= 100)>0 EUR<#else>5 EUR</#if>

Beispiel Prüfung, ob eine Zeichenkette ein bestimmtes Zeichen enthält

<#if name?contains('u')>
This is a name containing the letter 'u'
<#else>
No 'u' contained
</#if>

IF/ELSE mit String-Vergleichen:

<#if brandname! == ''>
Brandname is empty.
<#else>
${brandname!}
</#if>

Bei Vergleichen von Zeichenketten sollten Sie auf Leerzeichen achten und diese ggf. mit ?trim entfernen bzw. “wegschneiden”. 

<#if brandname!?trim == ''>
Brandname is really empty (spaces trimmed).
<#else>
${brandname!}
</#if>

Prüfen, ob Zeichenkette mit einem bestimmten Prefix startet (siehe starts_with)

<#if brandname!?starts_with('myprefix')>
Yes, brandname starts with myprefix
<#else>
No, brandname does not start with the prefix.
</#if>

Zugriff auf Spalten / Variablen mit Sondernzeichen

Wenn man im Mapper mit Freemarker auf Spalten mit Sonderzeichen wie z.B. Bindestrich zugreifen will, dann muss man eine andere Syntax verwenden.

Beispiel:

${meine-spalte}

Dieser Ausdruck bringt Freemarker durcheinander, weil er denkt man müsste (in Worten) “meine minus spalte” rechnen. 

Um mit Freemarker auf Variablen (Spalten) mit Sonderzeichen zuzugreifen kannst man eine andere Syntax verwenden z.B.:

${row["meine-spalte"]}

In der Variable ${row} (kommt von Synesty) sind alle Spalten enthalten und man kann mit eckigen Klammern darauf zugreifen. 

Ausgeschlossene Freemarker Build-Ins und Direktiven

Es können nahezu alle Direktiven bzw. Build-Ins der Freemarker Version 2.3.29 verwendet werden. Aus Sicherheitsgründen gibt es einige, wenige Ausnahmen die hauptsächlich im Bereich “Seldom used and expert built-ins” zu finden sind.

Explizit ausgeschlossen sind die Build-Ins:

und die Direktiven:

Automatische JSON und XML Erstellung

Mit Synesty lassen sich auch JSON und XML Payloads automatisch aus den Spalten eines Spreadsheets erstellen. Hierbei wird ein Schema aus einer Beispieldatei (JSON oder XML) erstellt, das die Struktur-Informationen des ursprünglichen Formats abspeichert. Wird nun ein Spreadsheet oder Datastore mit diesem target-Schema initialisiert, kann daraus wieder eine Datei oder Payload generiert werden, die dem Ausgangsformat entspricht.

Flatten und Unflatten

Was ist Flatten und Unflatten?

Als Flatten und Unflatten bezeichnet man die Vorgänge, bei denen komplex verschachtelte Datenstukturen (zum Beispiel JSON oder XML) entweder abgeflacht (flatten) oder bereits abgeflachte Strukturen wieder zurück in eine komplexe Form (unflatten) gebracht werden.

In Synesty liegen die Daten, die verarbeitet werden sollen, in 2-dimensionalen Spreadsheets vor. Das heißt, es existiert ein Spalten-Name und ein Feld mit einem dazugehörigen Wert. Jeder Datensatz liegt damit in einer flachen Zeile vor.

Werden komplexe Datenformaten wie JSON oder XML in ein Spreadsheet eingelesen, gehen damit die Struktur-Informationen des Ausgangsformates verloren.

Ein einfaches Abflachen (Flatten) geschieht bereits beim Parsen in ein Spreadsheet.

Angenommen, wir haben ein einfaches JSON:

{
"key1": {
"key2": "value"
}
}

Flattened würde diese Datenstruktur im Spreadsheet so aussehen:

key1_key2
value
{
"key_1": {
"key2": "value"
}
}

Enthält ein Key bereits ein ”_”, wird ein Punkt ”.” als Trenner benutzt.

key_1.key2
value

Der ganze Pfad bis hin zum Wert wird somit abgeflacht. Die Steps XML2Spreadsheet und JSON2Spreadsheet nutzen bereits eine einfache Logik, um die beiden Dateiformate in ein Spreadsheet zu überführen.

Mit dieser Logik lässt sich das Abflachen aber auch wieder rückgängig machen. Ist der Trenner bekannt, kann die Ausgangsstruktur wieder verschachtelt und aus dem 2-dimensionaen Spreadsheet wieder ein verschachteltes JSON oder XML erstellt werden (unflatten).

Der Trenner gibt somit eine neue Ebene des Pfades an.

key1_key2 -> key1
|_ key2

Das Schema als “Single-Source-of-Truth”

bei der Erstellung eines Schemas aus einer JSON oder XML wird diese Logik bereits berücksichtigt. Das heißt bei einem Schema, welches auf diesem Weg erstellt wurde, wird jeder Pfad abgeflacht und als Spalten-Name gesetzt. Zusätzlich werden dabei auch die Datentypen erkannt, was besonders für das JSON-Format relevant ist.

Damit wird das Schema genutzt, um die Struktur-Informationen der Ausgangs-Datei abzuspeichern.

Die Datentyp-Erkennung allein ist bereits sehr sinnvoll, da bei einem Mapper mit festgelegtem Target-Schema die Feld-Werte nach Datentyp validiert werden können.

Ein weiterer Vorteil ist nun, dass die Schema-Informationen auch im Freemarker (über die row.val(“col”)-Funktion) verfügbar sind.

Es ist natürlich auch möglich, sich auf diesem Weg ohne vorherigen Import selbst ein Schema anzulegen.

Damit mutiert das Schema von einer einfachen Spalten-Datentyp-Zuordnung zu einem zentralen Punkt, um automatisiert typsichere sowie struktur-valide Dateien und Payloads zu erzeugen. In Verbindung mit dem Template-Generator wird der manuelle Aufwand mittels Freemarker komplexe Strukturen nachzubauen wird dadurch auf ein Minimum reduziert, fällt in den meisten Fällen sogar ganz weg.

Flatten von JSON in ein Schema

Bei der Erstellung eines Schemas aus einer JSON-Datei wird eine flatten-Logik genutzt und die komplexe Baum-Struktur abgeflacht. Jede neue Ebene wird standardmäßig durch einen Unterstrich (”_”) getrennt. Wird in den Key Namen ein Unterstrich erkannt, wird als Trenner automatisch ein Punkt (”.”) gesetzt. Außerdem wird anhand der Werte versucht, den Content-Type (Art des Inhalts) und der Field-Type (Typ) automatisch zu ermitteln.

{
"level1": {
"level2": {
"level3": [
{
"key1": "name",
"key2": 12.5
},
{
"key1": "name2",
"key2": 0.5
}
],
"key3": 1
}
}
}

Diese Beispiel-JSON wird so als Schema eingelesen:

Spalten-Name Content-Type Field-Type
level1_level2_level3_key1 TEXT MULTIPLEVALUE
level1_level2_level3_key2 DECIMAL MULTIPLEVALUE
level1_level2_key3 INTEGER SINGLE

Die Option “Array Klammern beibehalten” ist sinnvoll, wenn aus dem Schema direkt wieder eine JSON erstellt werden soll. Die Aktivierung ist aber nur nötig, wenn mehrmals verschachtelte Strukturen in der JSON vorhanden sind.

{
"level1": {
"array1": [
{
"array2": [
{
"key": "foo"
},
{
"key": "bar"
}
],
"array3": [
{
"key": "foo"
},
{
"key": "bar"
}
]
}
]
}
}
Spalten-Name Content-Type Field-Type
level1_array1[]_array2_key1 TEXT MULTIPLEVALUE
level1_array1[]_array2_key2 TEXT MULTIPLEVALUE
level1_array1[]_array3_key1 TEXT MULTIPLEVALUE
level1_array1[]_array3_key2 TEXT MULTIPLEVALUE

Zu beachten ist hierbei, dass die Werte des letzten Arrays im vollen Pfad automatisch zu einem MULTIPLEVALUE Feld eingelesen werden. Aufgrund der Limitierung, die ein flaches Spreadsheet mit sich bringt, können sehr komplex verschachtelte JSON-Strukturen nicht vollständig abgebildet werden. MULTIPLEVALUE Felder können später mit der zipMap() Funktion wieder in mehrere Elemente eines Arrays umgeformt werden. Felder, bei denen die eckigen Klammern gesetzt wurden, die also als Array erkannt wurden, können durch ein einfaches Spreadsheet nicht mit mehreren Elementen gefüllt erstellt werden. Diese werden aber insofern berücksichtigt, dass die ursprüngliche Struktur wieder hergestellt bzw. ein Array mit nur einem Element erstellt wird. Somit sollte die aus einem Spreadsheet erstellte JSON Payload valide sein.

Flatten von XML in ein Schema

<root>
<level1>
<level2>
<key1>name</key1>
<key2>1</key2>
</level2>
<key3>1.5</key3>
</level1>
<key4>foo</key4>
</root>
Spalten-Name Content-Type Field-Type
root_level1_level2_key1 TEXT SINGLE
root_level1_level2_key2 INTEGER SINGLE
root_level1_key3 DECIMAL SINGLE
root_key4 TEXT SINGLE

Die Option “Root-Element auslassen” kann genutzt werden, um das umschließende XML-Root-Element nicht mit in die Spalten-Namen aufzunehmen und damit den Names etwas zu verkürzen. Soll im Anschluss wieder eine XML Payload erstellt werden, sollte diese Option nicht aktiviert werden.

Spalten-Name Content-Type Field-Type
level1_level2_key1 TEXT SINGLE
level1_level2_key2 INTEGER SINGLE
level1_key3 DECIMAL SINGLE
key4 TEXT SINGLE

Mit der Option “XML-Attribute einschließen” wird aus vorhandenen Attributen eine extra Spalte erstellt, denen eine ”@” vorgesetzt ist. Soll im Anschluss wieder eine XML Payload erstellt werden, sollte diese Option aktiviert werden. Zudem können die Attribute einfach im Spreadsheet gesetzt werden.

<root>
<level1 attr1="foo">
<level2 attr2="bar">
<key1>name</key1>
<key2>1</key2>
</level2>
<key3 attr3="zap" attr4="syn">1.5</key3>
</level1>
</root>
Spalten-Name Content-Type Field-Type
root_level1@attr1 TEXT SINGLE
root_level1_level2@attr2 TEXT SINGLE
root_level1_level2_key1 TEXT SINGLE
root_level1_level2_key2 INTEGER SINGLE
root_level1_key3 DECIMAL SINGLE
root_level1_key3@attr3 TEXT SINGLE
root_level1_key3@attr4 TEXT SINGLE

JSON aus MAP-Objekten

Mithilfe der toJSON()-Funktion lassen sich Objekte (MAP oder LIST) in JSON umwandeln. newMap() und zipMap() werden dabei verwendet, um komplexe Objekte zu konstruieren.

toJSON()

Der toJSON()-Funktion können 2 Parameter übergeben werden. Der erste Parameter ist das Objekt, aus dem das JSON erzeugt werden soll. Der zweite Parameter ist ein Objekt mit Konfigurations-Einstellungen.

MAP als Objekt-Parameter
${toJSON({"key1": "value1"})}
${toJSON(newMap({"key1": "value1"}))}
<#-- MAP_TYPE_COLUMN (TEXT) with value "key1=value1" -->
${toJSON(MAP_TYPE_COLUMN.val())}
{
"key1": "value1"
}
LIST als Objekt-Parameter
${toJSON([1,2,3])}
${toJSON("1,2,3"?split(","))}
<#-- MULTIPLEVALUE_TYPE_COLUMN (INTEGER) with value "1,2,3" -->
${toJSON(MULTIPLEVALUE_TYPE_COLUMN.val())}
[
1,
2,
3
]
Komplexe Objekte als Objekt-Parameter
${toJSON({
"key1": "value1",
"key2": [1,2,3],
"key3": {
"key4":{
"key5": "value5"
},
"key6": "value6"
},
"key7": "value7"
})}
{
"key1": "value1",
"key2": [
1,
2,
3
],
"key3": {
"key4": {
"key5": "value5"
},
"key6": "value6"
},
"key7": "value7"
}
Typsicherheit des erzeugten JSON

Ein herausragender Vorteil gegenüber der “alten” Methode, JSON zu generieren ist der, dass bei vorhandenem Schema oder einer MAP die Datentypen berücksichtigt und alle Kommas immer korrekt gesetzt werden.

Im JSON-Format existieren 3 Datentypen. Es gibt BOOLEAN, NUMBER und STRING, wobei nur STRING in “Anführungszeichen” gesetzt werden.

${toJSON({
"string": "value1",
"boolean": true,
"integer": 1,
"decimal": 1.5
})}
{
"string": "value1",
"boolean": true,
"integer": 1,
"decimal": 1.5
}

Auführungszeichen und Kommas werden auf diese Weise immer valide gesetzt.

Es können auch Operationen Inline erfolgen:

<#assign list = ["value1","value2","value3"] />
${toJSON({
"string": list[0],
"boolean": list?size == 3,
"integer": list?size - 2,
"decimal": list?size / 2
})}
{
"string": "value1",
"boolean": true,
"integer": 1,
"decimal": 1.5
}

Ist bereits die MAP nicht korrekt konfiguriert, wird vor der Erstellung ein Fehler angezeigt und das JSON nicht erst als Payload zum Server geschickt und dort validiert.

Das toJSON()-Konfigurations-Objekt

Das Konfigurations-Objekt steuert die Ausgabe des erzeugten JSON. Standardmäßig sind alle Optionen deaktiviert (false). Daher können alle Optionen, die nicht aktiviert werden sollen, auch weggelassen werden.

pretty

Gibt an, ob das JSON mit Zeilenumbrüchen bzw. Whitespaces ausgegeben werden soll.

  • Ohne Einrückung

Dieser Modus wird bei produktiven Runs empfohlen, da aufgrund der Fehlenden Whitespaces und somit reduzierten Payload-Größe Traffic eingespart wird.

${toJSON({
"key1": "value1",
"key2": "value2"
}, {'pretty': false})}
{"key1": "value1","key2": "value2"}
  • Mit Einrückung
${toJSON({
"key1": "value1",
"key2": "value2"
}, {'pretty': true})}
{
"key1": "value1",
"key2": "value2"
}
removeNull

Diese Option entfern bei der JSON Erstellung automatisch alle Keys deren Wert null ist. Das entspricht zum Beispiel nicht existierenden Spalten.

${toJSON({
"key1": "value1",
"key2": null
}, {'removeNull': true})}
{"key1": "value1"}
removeEmpty

Diese Option ist die restriktivere Variant von removeNull. Hier werden zusätzlich auch alle Werte, die leer sind entfernt:

  • null
  • leere Strings ("")
  • leere Listen ([])
  • leere Maps ({})
${toJSON({
"key1": "value1",
"key2": null,
"key3": "",
"key4": [],
"key5": {}
}, {'removeEmpty': true})}
{"key1": "value1"}

Diese Option überschreibt auch removeNull, falls beide gesetzt wurde.

${toJSON({
"key1": "value1",
"key2": null,
"key3": "",
"key4": [],
"key5": {}
},{'removeEmpty': true, 'removeNull': true})}
{"key1": "value1"}

Verarbeitung von MAP-Objekten

Beide Funktionen können verwendet werden, um komplex verschachtelte Strukturen zu erstellen und diese in ein typsicheres und Struktur-valides JSON zu erstellen. Die Voraussetzung ist, dass MAPs oder Listen aus MAPs entweder aus einem ROW-Objekt (row.toMap()) oder mit der newMap()-Funktion zusammengesetzt werden.

newMap()

Die Freemarker-Funktion newMap() kann verwendet werden, um MAPs zu erstellen oder zu verändern.

Initialisierung

Wird newMap() ohne Parameter initialisiert, wird eine leere MAP erstellt.

${toJSON(newMap(), {'pretty': true})}
{}

Alternativ kann auch eine MAP als Parameter angegeben werden:

${toJSON(
newMap({
"key1": "value1",
"key2": "value2"
}),{'pretty': true})
}
{
"key1": "value1",
"key2": "value2"
}

in Kombination mit einem ROW-Objekt kann auch direkt eine MAP aus einer ROW erzeugt werden:

key1key2
value1value2
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(newMap(row.toMap()),{'pretty': true})}
</#list>
{
"key1": "value1",
"key2": "value2"
}

Setzen, ändern oder löschen von Werten

Da eine MAP eine Schlüssel-Wert-Zuordnung ist, muss der Schlüssel (Key) bekannt sein, dessen Wert verändert werden soll.

set()

Mit set()können neue Schlüssel-Wert-Paare in die MAP eingefügt werden oder vorhandene Werte geändert werden.

  • Hinzufügen neuer Keys

newMap().set("key", "value")

${toJSON(
newMap({
"key1": "value1"
}).set("key2", "value2")
,{'pretty': true})
}
{
"key1": "value1",
"key2": "value2"
}
  • Ändern von bereits vorhandenen Keys

newMap().set("key", "newValue")

${toJSON(
newMap({
"key1": "value1"
}).set("key1", "newValue")
,{'pretty': true})
}
{
"key1": "newValue"
}
setNonEmpty()

Mit setNonEmpty()können neue Schlüssel-Wert-Paare in die MAP eingefügt oder geändert werden. Außerdem kann ein Default-Wert angegeben werden, falls der eigentliche Wert null ist.

newMap().set("key", "value", "defaultValue")

Ist der Wert ungleich null, ist die Funktion identisch mit set().

${toJSON(
newMap({
"key1": "value1"
}).setNonEmpty("key2", "value2", "defaultValue")
,{'pretty': true})
}
{
"key1": "value1",
"key2": "value2"
}

Ist der Wert aber null, wird der Default-Wert gesetzt.

${toJSON(
newMap({
"key1": "value1"
}).setNonEmpty("key2", null, "defaultValue")
,{'pretty': true})
}
{
"key1": "value1",
"key2": "defaultValue"
}
delete()

Mit delete() können neue vorhandene Schlüssel aus der MAP entfernt werden. Ist der Key nicht vorhanden, wird kein Fehler ausgegeben und die MAP bleibt unverändert. Die delete()-Funktion kann entweder einen String für eine einzelne Spalte als Parameter verarbeiten, oder eine Liste aus Strings für mehrere Spalten, die entfernt werden sollen.

  • Entfernen einzelner Keys

newMap().delete("key")

${toJSON(
newMap({
"key1": "value1",
"key2": "value2"
}).delete("key2")
,{'pretty': true})
}
{
"key1": "value1"
}
  • Entfernen mehrerer Keys

newMap().delete(["key1", "key2", "key3"])

${toJSON(
newMap({
"key1": "value1",
"key2": "value2",
"key3": "value3"
}).delete(["key2", "key3"])
,{'pretty': true})
}
{
"key1": "value1"
}
deleteReservedCols()

Mit deleteReservedCols() werden alle reservierten Spalten entfernt. Das ist vor allem sinnvoll, wenn die Daten aus einem Datastore kommen und row.toMap()genutzt wird.

Namen der reservierten Spalten:

  • datastorename
  • identifier
  • identifier2
  • identifier3
  • datastoretags
  • parent_identifier
  • folder
  • master_folder
  • master_identifier
  • createdAt
  • lastupdatedAt
  • lastContentChangedAt
  • processedAt
  • processingstatus
  • valid

newMap().deleteReservedCols()

Der Template-Generator entfernt bereits automatisch alle Spalten, die ausschließlich für interne Zwecke verwendet werden.

<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(newMap(row.toMap()).deleteReservedCols(),{'removeEmpty': true, 'pretty': true})}
</#list>
print()

Mit der print()- Funktion kann die MAP zu einem String konvertiert und angezeigt werden. Die Nutzung von toJSON()ist hierbei nicht nötig. Sollte ausschließlich bei der Entwicklung eingesetzt werden.

newMap().print()

${newMap({
"key1": "value1",
"key2": "value2"
}).print()}
{key1=value1, key2=value2}
unflatten()

Mit der unflatten()- Funktion ist eine sehr mächtige Funktion um aus den Spalten-Namen und Separator wieder komplexe Strukturen zu erzeugen. unflatten() erwartet als Parameter einen String, der den Separator angibt. Standardmäßig werden Unterstrich _ oder Punkt . angenommen. Das sind die Separatoren, die auch beim Parsen von XML oder JSON in ein Schema verwendet werden.

  • Mit Unterstrich
${toJSON(
newMap({
"node_key1": "value1",
"node_key2": "value2",
"key3": "value3"
}).unflatten("_")
,{'pretty': true})
}
{
"node": {
"key1": "value1",
"key2": "value2"
},
"key3": "value3"
}
  • Mit Punkt
${toJSON(
newMap({
"node.key_1": "value1",
"node.key_2": "value2",
"key_3": "value3"
}).unflatten(".")
,{'pretty': true})
}
{
"node": {
"key_1": "value1",
"key_2": "value2"
},
"key_3": "value3"
}

Generell kann aber auch jedes andere Zeichen dafür verwendet werden (auch wenn dieses Beispiel nicht sehr sinnvoll ist):

${toJSON(
newMap({
"nodekey1": "value1",
"nodekey2": "value2",
"key3": "value3"
}).unflatten("e")
,{'pretty': true})
}
{
"nod": {
"k": {
"y1": "value1",
"y2": "value2"
}
},
"k": {
"y3": "value3"
}
}

zipMap()

Die zipMap()-Funktion erstellt eine Liste aus Maps, welche dann entweder direkt mit toJSON()ausgegeben oder einem Key in einer anderen Map zugewiesen werden können. Der Erstellungsprozess ist Index-basiert. Das heißt, dass aus dem Wert jedes Index eine Map generiert und einer Liste hinzugefügt wird.

zipMap() erwartet eine MAP mit wenigstens einem Schlüssel-Wert Paar als ersten Parameter. Der 2. Parameter ist wie bei newMap() optional ein Objekt mit Konfigurations-Parametern.

Funktionsweise

key1=[1,2,3,4],
key2=["a","b","c","d"],
key3=["A","B","C","D"]

zipMap() erstellt nun für jeden Index eine MAP als Wert für den dazugehörigen Key.

Der Wert für Index 0 (die erste Position jeder Liste) wird extrahiert. Das entspricht den Werten:

  • key1=1
  • key2=“a”
  • key3=“A”

Der Wert für Index 1 (die zweite Position jeder Liste):

  • key1=2
  • key2=“b”
  • key3=“B”

Gleiche Anzahl von Elementen

Im einfachsten Fall hat jede Liste, die als Wert einem Key zugeordnet ist, dieselbe Anzahl von Elementen. Im folgenden Beispiel (4 Elemente) können jeder MAP jeweils 3 Elemente (3 Keys) zugewiesen werden.

Da eine Liste aus Maps erzeugt wird, kann das entstandene Objekt direkt mit toJSON() in ein JSON umgewandelt werden.

${toJSON(
zipMap({
"key1": [1,2,3,4],
"key2": ["a","b","c","d"],
"key3": ["A","B","C","D"]
}),{'pretty': true})
}

Es entsteht ein JSON-Array mit 4 Objekten (MAPS) mit jeweils 3 Einträgen:

[
{
"key1": 1,
"key2": "a",
"key3": "A"
},
{
"key1": 2,
"key2": "b",
"key3": "B"
},
{
"key1": 3,
"key2": "c",
"key3": "C"
},
{
"key1": 4,
"key2": "d",
"key3": "D"
}
]

Unterschiedliche Anzahl von Elementen

Werden die Datensätze (Listen) dynamisch aus einem Spreadsheet erstellt, könnte sich die Anzahl der Elemente unterscheiden. Für diese Fälle kann das Konfigurations-Objekt des 2. Parameters der zipMap()Funktion verwendet werden.

Standardmäßig sind alle Optionen deaktiviert und der 2. Parameter kann weggelassen werden.

Konstanten

Die Werte der Keys der zipMap() übergebenen MAP können neben Listen auch Einzelwerte sein. Im Unterschied dazu werden Einzelwerte als “Konstante” in jedes erstellte Objekt gesetzt.

${toJSON(
zipMap({
"key1": [1,2,3,4],
"key2": ["a","b","c","d"],
"key3": "CONSTANT"
}),{'pretty': true})
}
[
{
"key1": 1,
"key2": "a",
"key3": "CONSTANT"
},
{
"key1": 2,
"key2": "b",
"key3": "CONSTANT"
},
{
"key1": 3,
"key2": "c",
"key3": "CONSTANT"
},
{
"key1": 4,
"key2": "d",
"key3": "CONSTANT"
}
]

Das zipMap()-Konfigurations-Objekt

defaultNull

Diese Option legt einen Default-Wert fest, der gesetzt werden soll, falls Werte einer Liste null oder Indices nicht vorhanden sind (Listen unterschiedlicher Länge). Der an defaultNull übergebene Parameter kann entweder ein einzelner Wert sein, der überall gleich gesetzt wird:

${toJSON(
zipMap({
"key1": [1,null,3],
"key2": ["a","b","c","d"],
"key3": ["A","B"]
},{'defaultNull': "SYN"}
),{'pretty': true})
}

Im generierten 2. Element wurde der Wert null mit dem defaultNull Wert ersetzt. In den Elementen 3 und 4 wurden fehlende Indices ebenfalls ersetzt.

[
{
"key1": 1,
"key2": "a",
"key3": "A"
},
{
"key1": "SYN",
"key2": "b",
"key3": "B"
},
{
"key1": 3,
"key2": "c",
"key3": "SYN"
},
{
"key1": "SYN",
"key2": "d",
"key3": "SYN"
}
]

Es kann aber auch eine MAP als Konfigurations-Parameter übergeben werden. Damit lassen sich, für den Fall, dass Werte null sind oder nicht existieren, für jeden Key einzeln steuern:

${toJSON(
zipMap({
"key1": [1,null,3],
"key2": ["a","b","c","d"],
"key3": ["A","B"]
},{'defaultNull': {
'key1': "defaultKey1",
'key3': "defaultKey3"
}
}
),{'pretty': true})
}

key1 und key3 wurden verschiedene Werte zugewiesen:

[
{
"key1": 1,
"key2": "a",
"key3": "A"
},
{
"key1": "defaultKey1",
"key2": "b",
"key3": "B"
},
{
"key1": 3,
"key2": "c",
"key3": "defaultKey3"
},
{
"key1": "defaultKey1",
"key2": "d",
"key3": "defaultKey3"
}
]
skipEmpty

Ist diese Option aktiviert (true), werden Werte, die leer sind, nicht ausgegeben:

  • null
  • leere Strings ("")
  • leere Listen ([])
  • leere Maps ({})
${toJSON(
zipMap({
"key1": [1,2,{}],
"key2": ["a","b",[],""],
"key3": [null,"B"]
},{'skipEmpty': true}
),{'pretty': true})
}

Es werden nur 2 Elemente erzeugt und nur Werte ausgegeben, wenn kein Leer-Wert erkannt wurde.

[
{
"key1": 1,
"key2": "a"
},
{
"key1": 2,
"key2": "b",
"key3": "B"
}
]
skipNull

skipNull erwarte eine Liste mit Keys als String, welche nicht ausgegeben werden sollen, falls deren Wert null ist. Existiert der Key nicht in der an zipMap()übergebenen MAP, wird das ganze erzeugte Element (des aktuellen Index) nicht ausgegeben.

Die angegebenen Keys werden somit als required behandelt und alle anderen Werte verworfen.

${toJSON(
zipMap({
"key1": [1,2,3],
"key2": ["a","b","c","d"],
"key3": ["A","B"]
},{'skipNull': ["key3"]}
),{'pretty': true})
}

Der key3 wurde als skipNull Option übergeben. Da die Liste aber nur 2 Elemente beinhaltet, werden insgesamt auch nur 2 Elemente erzeugt.

[
{
"key1": 1,
"key2": "a",
"key3": "A"
},
{
"key1": 2,
"key2": "b",
"key3": "B"
}
]

Werden mehrere Keys angegeben, “gewinnt” jenes mit der geringeren Anzahl von Elementen:

${toJSON(
zipMap({
"key1": [1],
"key2": ["a","b","c","d"],
"key3": ["A","B"]
},{'skipNull': ["key1", "key3"]}
),{'pretty': true})
}

Der key1 hat nur ein Element. Dementsprechend wird auch nur 1 Element erzeugt.

[
{
"key1": 1,
"key2": "a",
"key3": "A"
}
]

So lässt sich sehr genau steuern, ob Array-Elemente erzeugt werden sollen oder nicht.

Template-Generator

Allgemeines

Der Template-Generator erstellt verschiedene Freemarker Script Beispiele zur automatischen Erstellung von XML oder JSON aus Spreadsheet Outputs. Dabei werden Schema-Informationen genutzt, um Templates für einfache oder komplexe Dateistrukturen zu generieren. Templates Aufrufen

Templates Aufrufen im Mapper

Generell ist der Template-Generator überall dort verfügbar, wo der Script-Editor vorhanden ist.

Einfache Templates

Je nach Verfügbarkeit werden mehr oder weniger Template Vorschläge angezeigt.

Erweiterte Templates

Einige Beispiele sind nur verfügbar, wenn sie sinnvoll aus den Daten des Quell-Spreadsheets generierbar sind. Beispiele die Child-Rows beinhalten werden nur beim SearchDatastore Step angezeigt, und das auch nur, wenn ein Master-Datatstore mit einem Child-Datastore verknüpft ist.

Die Beispiele, die komplex verschachtelte Datenstrukturen erstellen können (unflatten), werden nur dann angezeigt, wenn ein Feldtrenner (Underscore ”_” oder Punkt ”.”) in den Spalten-Namen erkannt wurde.

Mehr zum Thema Flatten/Unflatten finden Sie hier: Flatten und Unflatten

Template Beispiele

Die generierten Beispiele sollen grundlegende Herangehensweisen (einfache Beispiele) veranschaulichen, wie eine einfache (flache) Payload erstellt werden kann (Flache Beispiele) bzw. unter Berücksichtigung eines Schemas die Ausgangsstruktur wieder hergestellt werden kann (Komplexe Beispiele). In den meisten Fällen (Flache und Komplexe Beispiele) kann das automatisch erstellte Freemarker-Template bereits im Flow produktiv verwendet werden.

Einfache Beispiele

für die einfachen Beispiele nutzen wir unseren Beispiel-Datastore:

namebrandname
Super Shoe XIK-Star

Iteration über ein Spreadsheet

<#list spreadsheet@SearchMasterDatastore.getRows() as row>
row ${row.rowNumber!} : name: ${row.get("name")!}
row ${row.rowNumber!} : brandname: ${row.get("brandname")!}
</#list>

Dieses Beispiel listet über jede Row und gibt die aktuelle Zeilennummer, den Spalten-Namen sowie den Spaltenwert als TEXT aus.

Iteration über Spreadsheet (mit if/else-Logik)

<#list spreadsheet@SearchMasterDatastore.getRows() as row>
<#if row.get("name") != ''>${row.get("name")}</#if>
<#if row.get("brandname") != ''>${row.get("brandname")}</#if>
</#list>

Dieses Beispiel listet über jede Row und gibt den Spaltenwert nur aus, wenn der Wert nicht leer ist. Die ${row.get("colname")} -Funktion berücksichtigt nicht die Schema-Informationen und gibt jeden Wert als TEXT zurück.

Flache Beispiele

Erstellung von JSON

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(
newMap({
"name": row.val("name"),
"brandname": row.val("brandname")
}
),{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

In diesem Beispiel wird wieder über alle Rows iteriert, um ein einfaches, flaches JSON zu erstellen. Flach deshalb, da keine verschachtelte Struktur vorhanden ist. Es werden toJSON() und newMap() genutzt, um zuerst eine Map aus den Spalten-Namen und dazugehörigen Werten zu erstellen. Diese Map wird im Anschluss in ein JSON umgewandelt.

Mit dieser Variante wird ein typsicheres JSON mithilfe der Schema-Informationen erstellt. Die ${row.val("colname")} -Funktion gibt automatisch den im Schema hinterlegten Datentyp aus. Auf diese Art müssen weder die Kommas im JSON gesetzt werden, noch muss berücksichtigt werden, ob der Wert ein String oder eine Zahl ist. Anführungszeichen brauchen nicht gesetzt zu werden.

Die newMap()-Parameter 'removeEmpty': true sowie 'pretty': true legen fest, dass alle leeren Werte (”, [], {}) automatisch entfernt werden und das erstellte JSON anschaulich eingerückt wird.

Die Freemarker Direktive <#sep>,</#sep> setzt innerhalb der List-Anweisung für jede Zeile ein Komma. Ausgenommen der letzten und abschließenden Zeile (so wie es der JSON-Standard erwartet).

Füür mehr als eine Zeile würde dieses JSON aus dem Freemarker Code erstellt werden:

{
"name" : "Super Shoe XI",
"brandname" : "K-Star"
},
{
"name" : "Vintage",
"brandname" : "3-Stripes"
},
.
.
.
{
"name" : "Seapower",
"brandname" : "Check"
}

Da das kein valides JSON ist, das Template aber beide Fälle (1 Row und mehrere Rows) abdecken soll, haben wir einen den Hinweis als Kommentar abgegeben.

<#-- optional: surround with [] for multiple rows (arrays) -->

Für mehrere Rows müssen die Daten aller Rows in ein Array gesetzt werden.

Der Freemarker Code sollte dann so aussehen:

[
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(
newMap({
"name": row.val("name"),
"brandname": row.val("brandname")
}
),{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>
]

Die eckigen Klammern, die im JSON ein Array markieren, müssen außerhalb der List-Anweisung gesetzt werden.

Das Resultat sollte dann so aussehen:

[
{
"name": "Super Shoe XI",
"brandname": "K-Star"
},
{
"name": "Vintage",
"brandname": "3-Stripes"
},
.
.
.
{
"name": "Seapower",
"brandname": "Check"
}
]

So haben wir wieder ein valides JSON mit allen Rows als Elemente eines JSON-Arrays.

Liegt nur eine Row vor, wird mit dem ursprünglichen Freemarker Code ebenfalls ein valides JSON erstellt.

{
"name" : "Super Shoe XI",
"brandname" : "K-Star"
}

Mit nur wenigen Anpassungen lassen sich auf diese Weise für alle möglchen Anwendungsfälle valide JSON-payloads erstellen.

Erstellung von XML

<#ftl output_format="XML">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<#-- optional: surround with extra <root></root> tag for multiple rows -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
<row>
<name>${row.get("name")}</name>
<brandname>${row.get("brandname")}</brandname>
</row>
</#list>

Ähnlich wie bei der Erstellung von einfachen JSON-Formaten können aus dem gleichen Datensatz auch XML erstellt werden.

Die Zeile <#ftl output_format="XML"> teilt dem Freemarker-Renderer mit, dass das Ausgabeformat XML sein soll. Damit werden im XML ungültige Zeichen escaped und es muss bei Sonderzeichen kein <![CDATA[]]>-Abschnitt gesetzt werden. Alle Werte sind automatisch XML-valide.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<row>
<name>Super Shoe XI</name>
<brandname>K-Star</brandname>
</row>
<row>
<name>Vintage</name>
<brandname>3-Stripes</brandname>
</row>
.
.
.
<row>
<name>Seapower</name>
<brandname>Check</brandname>
</row>

Bei mehreren Rows ist die erstellte XML, genau wie bei JSON, nicht valide. XML erwartet ein alle Elemente umschließendes “Root”-Tag. Auch hier ist wieder ein Kommentar angegeben, wie in diesem Fall damit umzugehen ist.

<#-- optional: surround with extra <root></root> tag for multiple rows -->
<#ftl output_format="XML">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<root>
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
<row>
<name>${row.get("name")}</name>
<brandname>${row.get("brandname")}</brandname>
</row>
</#list>
</root>

Mit umschließendem Root-Tag ist die XML nun valide.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<root>
<row>
<name>Super Shoe XI</name>
<brandname>K-Star</brandname>
</row>
<row>
<name>Vintage</name>
<brandname>3-Stripes</brandname>
</row>
.
.
.
<row>
<name>Seapower</name>
<brandname>Check</brandname>
</row>
</root>

Die Tag-Namen “root” und “row” sind generisch und sollten entsprechend dem Ziel-Format angepasst werden.

Für eine Row wäre die XML mit dem unveränderten Freemarker Code wieder valide.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<row>
<name>Super Shoe XI</name>
<brandname>K-Star</brandname>
</row>

Erstellung einer XML (mit if/else-Logik)

<#ftl output_format="XML">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<#-- optional: surround with extra <root></root> tag for multiple rows -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
<row>
<#if row.get("name") != ''><name>${row.get("name")}</name></#if>
<#if row.get("brandname") != ''><brandname>${row.get("brandname")}</brandname></#if>
</row>
</#list>

Dieses Beispiel erstellt dasselbe Format wie Erstellung von XML. Der einzige Unterschied ist der, dass hier die Tags nur ausgegeben werden, wenn der entsprechende Wert nicht leer ist.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<row>
<name>Super Shoe XI</name>
</row>

Angenommen brandname ist leer, würde diese Row im XML so aussehen.

Erstellung eines einfachen JSON (alte Methode)

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
{
<#if row.get("name") != ''>"name": ${row.val("name")},</#if>
<#if row.get("brandname") != ''>"brandname": ${row.val("brandname")}</#if>
}<#sep>,</#sep>
</#list>

Zu Beispiel-Zwecken gibts es auch noch die alte Methode, um JSON zu erstellen.

Komplexe Beispiele

Die Beispiele, die komplex veraschachtelte Strukturen erstellen sind nur verfügbar, wenn einer der Feldtrenner (”_” oder ”.”) in den Spalten-Namen gefunden wurde. Diese Templates nutzen die Unflatten Logik.

Für die komplexen Beispiele nutzen wir unseren Beispiel-Datastore mit veränderten Spalten-Namen, um das Unflattening zu ermöglichen:

names_namenames_brand_brandname
Super Shoe XIK-Star

Erstellung eines komplexen JSON (mit Feldtrenner)

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(
newMap({
"names": newMap({
"name": row.val("names_name"),
"brand": newMap({
"brandname": row.val("names_brand_brandname")
})
})
})
,{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

Bei diesem Beispiel werden die Spalten-Namen wieder entsprechend dem erkannten Separator aufgetrennt und in mehreren Ebenen ausgegeben. Für jede Ebene wird mit newMap() eine neue Map erstellt. Ist der Pfad vollständig aufgelöst, wird der Wert Schema-valide ausgegeben.

Das resultierende JSON sieht für eine einzelne Row dann so aus:

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
}
}

Fügen wir den beiden Spalten 2 weitere hinzu. Beide sind vom Typ MULTIPLEVALUE und Content-Type INTEGER:

names_namenames_brand_brandnamewarehouse_idwarehouse_quantity
Super Shoe XIK-Star1;2;30;100;23

Das generierte Template sieht nun so aus:

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(
newMap({
"names": newMap({
"name": row.val("names_name"),
"brand": newMap({
"brandname": row.val("names_brand_brandname")
})
}),
"warehouse": zipMap({
"id": row.val("warehouse_id"),
"quantity": row.val("warehouse_quantity")
})
})
,{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

Im Vergleich zum ersten Beispiel wurde hier der neue Key warehouse in die Map angehängt, deren Wert aber die zipMap()-Funktion nutzt. Da diese Spalten MULTIPLEVALUE und dem Key warehouse zugeordnet sind, wird nun angenommen das es im Ausgangsformat ein Array mit mehreren eintragen gewesen ist. Die zipMap() Funktion erstellt nun aus jedem Eintrag der MULTPLEVALUE Spalten jeweils ein einzelnes Element.

Das Resultat sieht schlielßlich so aus:

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"warehouse": [
{
"id": 1,
"quantity": 0
},
{
"id": 2,
"quantity": 100
},
{
"id": 3,
"quantity": 23
}
]
}

Der Key warehouse wird so automatisch zu einem Array mit 3 Elementen.

Es können auch Spalten vom Typ MAP genutzt werden. Diese können aber nicht durch die Konvertierung von JSON/XML zu einem Schema erstellt werden. Wird das Schema aber manuell erstellt und der Typ festgelegt, können die Maps auch direkt mit eingebunden werden.

Fügen wir den beiden Ausgangs-Spalten eine weitere vom Typ MAP hinzu:

names_namenames_brand_brandnamewarehouse
Super Shoe XIK-Starid=1;quantity=0
<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(
newMap({
"names": newMap({
"name": row.val("names_name"),
"brand": newMap({
"brandname": row.val("names_brand_brandname")
})
}),
"warehouse": row.val("warehouse")
})
,{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

Die Spalte warehouse wird als MAP erkannt und automatisch zugeordnet:

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"warehouse": {
"id": 1,
"quantity": 0
}
}

Eine weitere Möglichkeit komplexere Daten in einem Mapper vorzubereiten ist es, im Schema ein Feld vom Typ MAP und den Content-Type JSON zu erstellen. Diese Spalte sollte dann einen JSON-String enthalten. Da der Typ als MAP festgelegt ist, wird der Wert beim Aufruf der row.val(“col”)-Funktion automatisch in eine Map umgewandelt.

names_namenames_brand_brandnamewarehouse
Super Shoe XIK-Star{“id”: 1,“quantity”:0}

Das Resultat ist dasselbe wie beim vorherigen Beispiel:

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"warehouse": {
"id": 1,
"quantity": 0
}
}

Wird ein Schema automatisch aus einer Beispiel-JSON erstellt, besteht die Möglichkeit, die Option “Array-Klammern beibehalten” zu aktivieren. Angenommen, es soll dieses JSON in ein Schema eingelesen werden:

{
"names":
{
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"warehouses": [
{
"locations": [
{
"id": 0,
"quantity": 0
},
{
"id": 1,
"quantity": 5
}
]
},
{
"locations": [
{
"id": 4,
"quantity": 3
},
{
"id": 5,
"quantity": 1
}
]
}
]
}

Der Knoten warehouses besteht aus mehreren verschachtelten Arrays. Ohne “Array-Klammern beibehalten” würde die Datei in diese Spalten überführt werden:

Ohne Klammern
FeldTyp
names_nameSINGLE
names_brand_brandnameSINGLE
warehouses_locations_idMULTIPLEVALUE
warehouses_locations_quantityMULTIPLEVALUE

Der “einfache” Modus erkennt für jeden Key, dessen Wert in einem Array enthalten ist automatisch den Typ MULTIPLEVALUE. Ist die Option aktiviert, wird an jeden Knoten, der in dert Ausgangsdatei ein Array ist, ein ”[]” angehängt:

Mit Klammern
FeldTyp
names_nameSINGLE
names_brand_brandnameSINGLE
warehouses[]_locations[]_idMULTIPLEVALUE
warehouses[]_locations[]_quantityMULTIPLEVALUE

Beide Modi behandeln bei der Feld-Erkennung Arrays leicht unterschiedlich. Felder vom Typ MULTIPLEVALUE sind in erster Linie dazu gedacht, später im Freemarker Template mithilfe der zipMap()-Funktion wieder in ein Array umgeformt zu werden. Aufgrund der Limitierung von Spreadsheets Daten nur flach abbilden zu können, ist es nur sinnvoll zipMap() zu nutzen, wenn sich der voll aufgelöste Pfad zum Wert in einem Array befindet.

Ohne die Klammern würde dieses Template erstellt werden:

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(
newMap({
"names": newMap({
"name": row.val("names_name"),
"brand": newMap({
"brandname": row.val("names_brand_brandname")
})
}),
"warehouses": newMap({
"locations": zipMap({
"id": row.val("warehouses_locations_id"),
"quantity": row.val("warehouses_locations_quantity")
})
})
})
,{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

Setzen wir nun die Ausgangswerte wieder ein:

names_namenames_brand_brandnamewarehouses_locations_idwarehouses_locations_quantity
Super Shoe XIK-Star0;10;5

Wird dieses JSON aus den Daten erstellt:

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"warehouses": {
"locations": [
{
"id": 0,
"quantity": 0
},
{
"id": 1,
"quantity": 5
}
]
}
}

Hier sind nun bereits die beiden Beschränkungen zu erkennen, wenn flache Datenstrukturen automatisch “unflattended” werden sollen. Zum einen war der Knoten warehouses ursprünglich ein Array. Zum Anderen können dem Key warehouses nicht mehrere Elemente zugeordnet werden. Vermutlich würde dieses JSON versendet als Payload als invalide abgelehnt werden, obwohl alle Daten auf den ersten Blick korrekt aussehen.

Ist die Option “Array-Klammern beibehalten” jedoch aktiviert und es werden dieselben Daten gesetzt:

names_namenames_brand_brandnamewarehouses[]_locations[]_idwarehouses[]_locations[]_quantity
Super Shoe XIK-Star0;10;5

Wir erhalten dieses Template:

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(
newMap({
"names": newMap({
"name": row.val("names_name"),
"brand": newMap({
"brandname": row.val("names_brand_brandname")
})
}),
"warehouses": [newMap({
"locations": zipMap({
"id": row.val("warehouses[]_locations[]_id"),
"quantity": row.val("warehouses[]_locations[]_quantity")
})
})]
})
,{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

Und schließlich dieses JSON:

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"warehouses": [
{
"locations": [
{
"id": 0,
"quantity": 0
},
{
"id": 1,
"quantity": 5
}
]
}
]
}

Die Beschränkung, dass innere Array aus diese Art nur ein Element beinhalten können, besteht zwar weiterhin, aber die Payload entspricht dennoch dem Ausgangsformat und ist somit valide.

Erstellung eines komplexen JSON aus einer ROW (mit Feldtrenner)

Dies ist eine weitere, schlankere Methode aus einer ROW ein komplexes JSON zu erstellen:

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
${toJSON(newMap(row.toMap()).deleteReservedCols().unflatten('_'),{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

Hier wird die ROW-Methode toMap() genutzt, um als Spalten eines ROW zuerst in eine MAP zu konvertieren, um darauf dann die newMap()-Methoden .deleteReservedCols() und .unflatten('_') anzuwenden.

.deleteReservedCols() entfernt alle reservierten Spalten (insofern die Daten aus einem SearchDatastore-Step kommen).

.unflatten() erstellt dann im Anschluss aus den flachen Spalten-Namen wieder eine verschachtelte MAP, die wiederum mit toJSON() in ein typsicheres JSON umgewandelt wird.

Für beide Varianten (mit und ohne Array-Klammern) werden diese JSON erstellt:

Ohne Klammern
{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"warehouses": {
"locations": [
{
"id": 0,
"quantity": 0
},
{
"id": 1,
"quantity": 5
}
]
}
}
Mit Klammern
{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"warehouses": [
{
"locations": [
{
"id": 0,
"quantity": 0
},
{
"id": 1,
"quantity": 5
}
]
}
]
}

Beide Ausgaben sind identisch mit denen der vorherigen komplexeren Templates. Diese Methode bietet allerdings kaum Möglichkeite, über den Freemarker-Code Einfluss auf die Ausgabe zu nehmen. Sie ist aber zu empfehlen wenn ausschließlich Schema-Spalten vorhanden sind und ist auch weniger fehleranfällig, da die ganze Logik durch das Synesty-Studio abgehandelt wird.

Erstellung eines komplexen XML (mit Feldtrenner)

Ein automatisch erstelltes Schema aus einer XML derselben Struktur wie im vorhergehenden JSON Beispiel würde diese Felder erzeugen:

FeldTyp
names_nameSINGLE
names_brand_brandnameSINGLE
warehouses_locations_idSINGLE
warehouses_locations_quantitySINGLE

Da XML im Gegensatz zu JSON keine Arrays kennt und so aus MULTIVALUE Spalten keine Arrays erzeugt werden müssen, wird auch jedes XML-Tag als SINGLE eingelesen.

Wir nutzen weiterhin diese Daten:

names_namenames_brand_brandnamewarehouses_locations_idwarehouses_locations_quantity
Super Shoe XIK-Star00
<#ftl output_format="XML">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<#-- optional: surround with extra <root></root> tag for multiple rows -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
<row>
<names>
<name>${row.get("names_name")}</name>
<brand>
<brandname>${row.get("names_brand_brandname")}</brandname>
</brand>
</names>
<warehouses>
<locations>
<id>${row.get("warehouses_locations_id")}</id>
<quantity>${row.get("warehouses_locations_quantity")}</quantity>
</locations>
</warehouses>
</row>
</#list>

Die Spalten werden auch hier wieder automatisch “unflattened” und verschachtelte XML-Strukturen generiert. Da XML keine Datentypen wie JSON kennt, werden alle Werte mit row.get("col") als reiner TEXT eingesetzt.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<row>
<names>
<name>Super Shoe XI</name>
<brand>
<brandname>K-Star</brandname>
</brand>
</names>
<warehouses>
<locations>
<id>0</id>
<quantity>0</quantity>
</locations>
</warehouses>
</row>

Fügen wir noch 2 weitere hinzu, um Tag-Attribute zu setzen:

names_namenames_name@activenames_brand_brandnamewarehouses_locations_idwarehouses_locations@countrywarehouses_locations_quantity
Super Shoe XIfalseK-Star0DE0
<#ftl output_format="XML">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<#-- optional: surround with extra <root></root> tag for multiple rows -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
<row>
<names>
<name active="${row['names_name@active']}">${row.get("names_name")}</name>
<brand>
<brandname>${row.get("names_brand_brandname")}</brandname>
</brand>
</names>
<warehouses>
<locations country="${row['warehouses_locations@country']}">
<id>${row.get("warehouses_locations_id")}</id>
<quantity>${row.get("warehouses_locations_quantity")}</quantity>
</locations>
</warehouses>
</row>
</#list>

In dieser Form werden Tags automatisch dem entsprechenden Tag zugeordnet.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<row>
<names>
<name active="false">Super Shoe XI</name>
<brand>
<brandname>K-Star</brandname>
</brand>
</names>
<warehouses>
<locations country="DE">
<id>0</id>
<quantity>0</quantity>
</locations>
</warehouses>
</row>

Es ist auch möglich, mehrere Attribute pro Tag zu setzen:

names_namenames_name@activenames_name@instocknames_name@languagenames_brand_brandnamewarehouses_locations_idwarehouses_locations_quantity
Super Shoe XIfalsetrueENK-Star00
<#ftl output_format="XML">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<#-- optional: surround with extra <root></root> tag for multiple rows -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
<row>
<names>
<name active="${row['names_name@active']}" instock="${row['names_name@instock']}" language="${row['names_name@language']}">${row.get("names_name")}</name>
<brand>
<brandname>${row.get("names_brand_brandname")}</brandname>
</brand>
</names>
<warehouses>
<locations>
<id>${row.get("warehouses_locations_id")}</id>
<quantity>${row.get("warehouses_locations_quantity")}</quantity>
</locations>
</warehouses>
</row>
</#list>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<row>
<names>
<name active="false" instock="true" language="EN">Super Shoe XI</name>
<brand>
<brandname>K-Star</brandname>
</brand>
</names>
<warehouses>
<locations>
<id>0</id>
<quantity>0</quantity>
</locations>
</warehouses>
</row>

Komplexe Beispiele (aus Master- und Child-Datastore)

Diese Beispiele sind nur verfügbar, wenn die Quelle ein SearchDatastore-Step ist und der ausgewählte Datastore mit einem Child-Datastore verknüpft ist. In allen folgenden Beispielen werden die Daten aus beiden Datastores zu einem Datensatz zusammengeführt. In der Praxis würde das zum Beispiel einer Auftragsanlage entsprechen, bei der Kopf- und Positions-Daten in 2 verschiedenen Datastores abgelegt sind.

Als Ausgangsdaten werden diese Schemas genutzt:

Master Datastore
names_namenames_brand_brandname
Super Shoe XIK-Star
Child Datastore
idname
10K-Star blue
11K-Star red

In diesem Beispiel werden Artikel-Daten mit Varianten dieses Artikels in einem Template zu einer peyload zusammengefügt.

Erstellung eines komplexen JSON mit eingebundenen Child-Rows (mit Feldtrenner)

Der Freemarker Code besteht aus 2 Blöcken. Im ersten wird eine leere Sequenz/Liste children erstellt. Dann wird für alle Child-ROWs der aktuellen ROW eine Map child erzeugt und der Liste children hinzugefügt. Als Resultat entsteht so eine Liste aus MAPs für jede Child-ROW.

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore.getRows() as row>
<#assign children = [] />
<#list row.children() as row>
<#assign child = newMap({
"id": row.val("id"),
"name": row.val("name")
}) />
<#assign children = children + [child] />
</#list>
${toJSON(
newMap({
"names": newMap({
"name": row.val("names_name"),
"brand": newMap({
"brandname": row.val("names_brand_brandname")
})
}),
"warehouse": row.val("warehouse")
}).set("children", children)
,{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

Im 2. Block wird dann im Anschluss mit der newMap().set('key', 'value')-Methode der Key children mit dem Wert der vorher erstellen children-Liste gesetzt. Der Key children ist generisch und sollte den Anforderungen entsprechend angepasst werden. Da wir hier Varianten haben, wird der Key auf variants gesetzt.

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"variants": [
{
"id": 10,
"name": "K-Star blue"
},
{
"id": 11,
"name": "K-Star red"
}
]
}

Erstellung eines komplexen JSON aus einer ROW mit eingebundenen Child-Rows (mit Feldtrenner)

Das ist wieder die schlankere Variante des vorherigen Beispiels. Es wird mit row.children()?map() der Freemarker-Lambda-Ausdruck genutzt um in einem Einzeiler für jede childRow (ohne reservierte Spalten) eine Liste aus MAPs zu erzeugt

<#-- optional: surround with [] for multiple rows (arrays) -->
<#list spreadsheet@SearchMasterDatastore_6.getRows() as row>
<#assign children = row.children()?map(childRow -> newMap(childRow.toMap()).deleteReservedCols()) />
${toJSON(newMap(row.toMap()).deleteReservedCols().unflatten('_').set('children', children),{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>

Im Anschluss wird die erzeuge Liste wieder dem Key children zugewiesen.

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"variants": [
{
"id": 10,
"name": "K-Star blue"
},
{
"id": 11,
"name": "K-Star red"
}
]
}

Das Resultat ist wieder identisch mit der ausführlicheren Variante im Beispiel zuvor.

Erstellung einer komplexen XML mit eingebundenen Child-Rows (mit Feldtrenner)

Im Unterschied zur Einbindung von verknüpften Child-Datastore-Zeilen bei der Erstellung von JSON, werden bei XML die Daten Inline eingebunden und nicht separat verarbeitet. Lassen sich aus dem Pfad im Spalten-Namen weder root noch row-Key extrahieren, werden automatisch die Platzhalter children und child gesetzt. Diese müssen dann wieder entsprechend der Vorgaben angepasst werden.

<#ftl output_format="XML">
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<#-- optional: surround with extra <root></root> tag for multiple rows -->
<#list spreadsheet@SearchMasterDatastore_6.getRows() as row>
<row>
<names>
<name>${row.get("names_name")}</name>
<brand>
<brandname>${row.get("names_brand_brandname")}</brandname>
</brand>
</names>
<children>
<#list row.children() as row>
<variant>
<id>${row.get("variant_id")}</id>
<name>${row.get("variant_name")}</name>
</variant>
</#list>
</children>
</row>
</#list>

Die fertige XML sieht gefüllt mit Daten dann so aus:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<row>
<names>
<name>Super Shoe XI</name>
<brand>
<brandname>K-Star</brandname>
</brand>
</names>
<children>
<variant>
<id>10</id>
<name>K-Star blue</name>
</variant>
<variant>
<id>11</id>
<name>K-Star red</name>
</variant>
</children>
</row>

JSON-XML Format-Konvertierung

Ein weiterer Vorteil der generischen Template-Erstellung aus den Schema-Feldern ist es, dass auch unter Beibehaltung der verschachtelten original Struktur Datei-Format-Konvertierungen vorgenommen werden können. Es kann eine XML oder JSON als Schema eingelesen werden und in jeweils das andere Format überführt werden.

Wie in den Beispielen mit der Einbindung der Child-Datastore-Zeilen zeigt:

Master Datastore
names_namenames_brand_brandname
Super Shoe XIK-Star
Child Datastore
idname
10K-Star blue
11K-Star red

Als JSON:

{
"names": {
"name": "Super Shoe XI",
"brand": {
"brandname": "K-Star"
}
},
"variants": [
{
"id": 10,
"name": "K-Star blue"
},
{
"id": 11,
"name": "K-Star red"
}
]
}

Als XML:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<row>
<names>
<name>Super Shoe XI</name>
<brand>
<brandname>K-Star</brandname>
</brand>
</names>
<children>
<variant>
<id>10</id>
<name>K-Star blue</name>
</variant>
<variant>
<id>11</id>
<name>K-Star red</name>
</variant>
</children>
</row>

XML sowie JSON haben automatisch dieselbe Struktur. Lediglich die generischen Key-Namen müssen im XML-Format angepasst werden.

Praxis-Beispiel: Artikelanlage

Mithilfe der automatischen Schema-Erstellung aus einer Beispiel-Datei und dem Template Generator lassen sich nun relativ einfach mit wenigen Klicks Dateien oder Payloads erstellen. Ein einfacher Fall wäre zum Beipiel die Anlage von Artikeln in einem Shop-System.

In der Regel wird bei der Artikelanlage eine Payload im XML oder JSON Format erwartet. In der Dokumentation der jeweiligen Systeme sind dann sehr wahrscheinlich auch Beispiele der zu erstellenden Payload zu finden.

Schritt 1: Ziel-Payload beziehen

[
{
"name": "Ultra",
"brandname": "Performance Beast",
"description": "Description of Ultra",
"price": 69.9,
"pricecurrency": "EUR",
"stock": 0,
"variants": {
"color": "white",
"size": "S"
}
},
{
"name": "Performance ",
"brandname": "Xtreme",
"description": "Description of Performance ",
"price": 104.45,
"pricecurrency": "EUR",
"stock": 999999,
"variants": {
"color": "black",
"size": "XXL"
}
}
]

Wie in diesem Beispiel wird das Format für die Anlage oder Aktualisierung von Artikeln gezeigt.

Die Beispiel-JSON wird zuerst entweder als Datei geladen oder herauskopiert und als JSON-Datei gespeichert. Die Datei wird benötigt um, im ersten Schritt ein Schema zu erstellen.

::: info Wichtiger Hinweis Zu beachten ist, dass eine möglichst vollständige Beispiel-Datei verwendet wird. Es sollten alle möglichen Felder vorhanden sein.

Wird die Payload erstellt, werden automatisch leere Felder entfernt. Fehlen Bei der Schema-Erstellung bereits relevante Felder, muss das Schema manuell angepasst und dem Mapper/Datastore neu zugewiesen werden. Auch das Template muss dann neu generiert werden. :::

Schritt 2: Schema erstellen

Im nächsten Schritt wird aus der Beispieldatei das Schema erstellt. Da hier das JSON-Format vorliegt, muss die Option JSON Schema importieren ausgewählt und die vorher bezogene JSON importiert werden. Da in der JSON keine Arrays vorhanden sind, muss die entsprechende Option beim Schema Import nicht aktiviert werden.

Artikel Payload Schema

Ist das Schema erstellt, sollte nochmal geprüft werden, ob der “Art des Inhalts” bei jedem Feld korrekt sind. Außerdem sollte in der Dokumentation nochmal geschaut werden, welche Felder Pflichtfelder sind und entsprechend im Schema gesetzt werden.

Damit haben wir nun eine “Vorlage” erstellt, welche die alle relevanten Informationen enthält, um die Daten später zu validieren und die ursprüngliche Format-Struktur mit dem Template Generator wiederherzustellen.

Schritt 3: Schema laden

Um das Schema zu nutzen, sollte entweder:

  1. Ein Datastore mit dem eben erstellten Schema erstellt oder
  2. Das Schema einem Mapper zugewiesen werden.

Mapper Schema

Schritt 4: Template erstellen

Wurden die Daten gemappt oder im Datastore gespeichert, kann jetzt das Template für die Payload erstellt werden.

Im Script-Textfeld eines TextHTMLWriter, APICall oder SpreadsheetUrlDownload Step wird ein Beispiel-Template für komplexe JSON geladen. Das JSON-Template wird automatisch erstellt. Da ein Spreadsheet mit mehreren Zeilen vorliegt und die originale Beispiel-JSON ein Array mit mehreren Artikeln beinhaltet, muss der ganze Script-Block noch in manuell eckige Klammern gesetzt werden.

<#-- optional: surround with [] for multiple rows (arrays) -->
[
<#list rows as row>
${toJSON(
newMap({
"name": row.val("name"),
"brandname": row.val("brandname"),
"description": row.val("description"),
"price": row.val("price"),
"pricecurrency": row.val("pricecurrency"),
"stock": row.val("stock"),
"variants": newMap({
"color": row.val("variants_color"),
"size": row.val("variants_size")
})
})
,{'removeEmpty': true, 'pretty': true})}<#sep>,</#sep>
</#list>
]

Schritt 5: Daten Mappen

Je nachdem ein Datastore erstellt oder das Schema einem Mapper zugewiesen wurde, werden die Spalten jetzt mit Daten befüllt bzw. gemappt.

Wird der Flow ausgeführt oder die Step-Vorschau angezeigt, wird folgendes JSON erstellt:

[
{
"name" : "Predator v3",
"brandname" : "Big cat",
"description" : "Description of Predator v3",
"price" : 30.99,
"pricecurrency" : "EUR",
"stock" : 57,
"variants" : {
"color" : "white",
"size" : "M"
}
},
{
"name" : "Predator v5",
"brandname" : "Big cat",
"description" : "Description of Predator v5",
"price" : 50.99,
"pricecurrency" : "EUR",
"stock" : 11,
"variants" : {
"color" : "red",
"size" : "M"
}
},
.
.
.
.
{
"name" : "Cheesy",
"brandname" : "3-Stripes",
"description" : "Description of Cheesy",
"price" : 777.77,
"pricecurrency" : "EUR",
"stock" : 67,
"variants" : {
"color" : "grey",
"size" : "XXL"
}
},
{
"name" : "Superplanet",
"brandname" : "3-Stripes",
"description" : "Description of Superplanet",
"price" : 99.99,
"pricecurrency" : "EUR",
"stock" : 35,
"variants" : {
"color" : "white",
"size" : "M"
}
}
]

Folgt man diesem Ablauf, können komplexe Dateien innerhalb weniger Minuten ohne Scripting- oder Programmier-Kenntnisse erstellt werden. Der Ablauf ist ebenfalls auf alle anderen Prozesse anwendbar, die ein einzelnes Spreadsheet als Datenquelle verwenden.

Praxis-Beispiel: Auftragsanlage

Der Ablauf ist prinzipiell identisch wie bei der Artikelanlage. Der Unterschied ist der, die Auftragsanlage in der Regel mit 2 Datastores (Master/Child) umgesetzt wird.

Schritt 1: Ziel-Payload beziehen und Daten extrahieren

Die Beispiel-JSON für die Auftragsanlage wird wieder aus der Dokumentation bezogen.

[
{
"name": "Max Mustermann",
"address": "Heidestraße 17",
"city": "München",
"items": [
{
"name": "Ultra",
"quantity": 2
},
{
"name": "Performance",
"quantity": 1
}
]
}
]

Da Kopf- und Positionsdaten in verschiedenen Datastores gespeichert werden, müssen beide Datensätze extrahiert werden und separat als JSON vorliegen, um die Schemas anzulegen.

Kopfdaten (Master-Datastore)

{
"name": "Max Mustermann",
"address": "Heidestraße 17",
"city": "München"
}

Positionsdaten (Child-Datastore)

[
{
"name": "Ultra",
"quantity": 2
},
{
"name": "Performance",
"quantity": 1
}
]

Schritt 2: Schemas erstellen

Beide JSON-Dateien werden wieder als JSON Schema importiert und gegebenenfalls angepasst.

Kopfdaten Payload Schema Positionsdaten Payload Schema

Schritt 3: Datastores erstellen

im nächsten Schritt müssen 2 Datastores in einer Master-Child Relation erstellt werden.

Dem Master-Datastore für die Kopfdaten wird gleich bei der Erstellung das entsprechende Schema zugewiesen.

Kopfdaten Master Datastore

Der Datastore für die Positionsdaten wird dem Kopfdaten-Master zugewiesen und wieder das vorher erstellte Schema gesetzt.

Positionsdaten Payload Schema

Schritt 4: Template erstellen

Um das Template zu erstellen, muss im SearchDatastore Step der Kopfdaten Datastore ausgewählt werden.

Wichtig hierbei ist, dass die Children nicht ausgegeben werden. Das würde dazu führen, dass die Children als normale Zeilen ausgegeben werden. Das erstellte Template holt sich die Daten mithilfe der row.children() Methode.

Kopfdaten SearchDatastore

Im Anschluss wird wieder im Script-Feld eines TextHTMLWriter, APICall oder SpreadsheetUrlDownload Steps ein Template ausgewählt, welches die children in die Payload integriert. Die komplexen Templates erstellen verschachtelte JSONs und verwenden die unflatten Logik. Im aktuellen Beispiel wird ein “flaches” JSON erstellt.

Kopfdaten Templates

Das automatisch generierte Template wird noch soweit angepasst, dass es in eckige Klammern gestetzt wird, um ein Array alles Master-Zeilen zu erzeugen. Außerdem muss der Key in .set("children", children) zu .set("items", children) geändert werden, um die children korrekt zuzuweisen.

<#-- optional: surround with [] for multiple rows (arrays) -->
[
<#list spreadsheet@SearchMasterDatastore_19.getRows() as row>
<#assign children = [] />
<#list row.children() as row>
<#assign child = newMap({
"name": row.val("name"),
"quantity": row.val("quantity")
}) />
<#assign children = children + [child] />
</#list>
${toJSON(
newMap({
"name": row.val("name"),
"address": row.val("address"),
"city": row.val("city")
}).set("items", children),{'removeEmpty': true, 'pretty': true}
)}<#sep>,</#sep>
</#list>
]

Schritt 5: Datastores befüllen

Schließlich werden beide Datastores sinnvoll mit Daten gefüllt und das JSON aus dem Template erstellt.

[
{
"name" : "Max Mustermann",
"address" : "Heidestraße 17",
"city" : "München",
"items" : [ {
"name" : "Predator v3",
"quantity" : 2
} ]
},
{
"name" : "Maria Musterfrau",
"address" : "Nußhäherstraße 10",
"city" : "Berlin",
"items" : [ {
"name" : "Superplanet",
"quantity" : 5
}, {
"name" : "Cheesy",
"quantity" : 1
} ]
}
]