Zum Inhalt

JSON und Objekte verstehen

Fast alles, was in KCForms zwischen Formular, Datenbank und Skript hin- und herwandert, ist ein Objekt — notiert in einer Schreibweise namens JSON. Das Formular-Schema ist JSON. Die Werte deines Formulars sind ein Objekt. Eine Detail-Tabelle ist eine Liste von Objekten. Eine Antwort aus dem Internet ist meistens JSON.

Wer diese eine Sache versteht, versteht alle diese Stellen auf einmal. Diese Seite erklärt sie von Grund auf — ohne Vorwissen, in kleinen Schritten. Die Sprache selbst — Variablen, Text, Zahlen, Datum, Schleifen, Funktionen — steht nebenan unter JavaScript-Grundlagen.

Zum Mitmachen

Alle Beispiele laufen in der Skript-Konsole: System-Administration → Werkzeuge & Diagnose → Skript-Konsole. Beispiel hineinkopieren, Ausführen klicken, unten die Ausgabe lesen. log.info(...) schreibt eine Zeile in diese Ausgabe.


Ein Objekt ist ein Karteikasten

Stell dir eine Karteikarte vor. Links steht die Beschriftung, rechts der Inhalt:

Beschriftung Inhalt
Kunde Anna Berg
Betrag 249.90
Bezahlt nein

Genau das ist ein Objekt. Aufgeschrieben in JSON sieht dieselbe Karte so aus:

{
  "kunde": "Anna Berg",
  "betrag": 249.90,
  "bezahlt": false
}

Die Bestandteile:

  • Geschweifte Klammern { } umschließen das ganze Objekt — sie sind der Karteikasten.
  • Links vom Doppelpunkt steht der Schlüssel (die Beschriftung), rechts der Wert (der Inhalt).
  • Zwischen zwei Einträgen steht ein Komma. Nach dem letzten nicht.

Im Skript schreibt man dasselbe ohne die Anführungszeichen um die Schlüssel und holt einen Wert mit einem Punkt heraus:

var bestellung = { kunde: "Anna Berg", betrag: 249.90, bezahlt: false };

log.info(bestellung.kunde);     // Anna Berg
log.info(bestellung.betrag);    // 249.9

Warum heißt es JSON?

JavaScript Object Notation — „die Schreibweise, in der JavaScript Objekte notiert". JSON ist reiner Text: dieselbe Karteikarte lässt sich damit speichern, per Mail verschicken oder durchs Internet schicken. Ein Objekt im Skript ist der lebendige Zwilling davon.


Was in einem Wert stehen darf

Es gibt genau sechs Arten von Werten. Mehr gibt es nicht — alles Weitere entsteht durch Kombination.

Art Beispiel Wofür
Text "Anna Berg" Namen, Beschreibungen, alles Geschriebene
Zahl 249.90, 7, -3 Beträge, Mengen, Zähler
Wahrheitswert true, false Ja/Nein, Häkchen im Formular
Nichts null „hier steht bewusst nichts"
Objekt { "ort": "Kiel" } eine Karte in der Karte
Liste [1, 2, 3] mehrere Werte hintereinander

Ein Punkt trennt die Nachkommastellen, nie ein Komma: 249.90, nicht 249,90. Texte stehen in doppelten Anführungszeichen.

Die letzten beiden Arten machen JSON mächtig — eine Karte darf weitere Karten und Listen enthalten:

{
  "kunde": "Anna Berg",
  "adresse": { "strasse": "Hafenweg 3", "plz": "24103", "ort": "Kiel" },
  "positionen": [
    { "artikel": "Stift", "menge": 3, "preis": 1.20 },
    { "artikel": "Block", "menge": 2, "preis": 2.50 }
  ]
}

Lies das von außen nach innen: eine Bestellung, darin eine Adresse (ein Objekt) und eine Positionsliste (eine Liste von Objekten). Genau so sieht ein KCForms-Formular mit Detail-Tabelle aus.


Einen Wert herausholen: Punkt oder eckige Klammer

Es gibt zwei Schreibweisen, und sie bedeuten dasselbe:

var bestellung = { kunde: "Anna Berg", betrag: 249.90 };

log.info(bestellung.kunde);        // Punkt-Schreibweise
log.info(bestellung["kunde"]);     // Klammer-Schreibweise — dasselbe Ergebnis

Die Punkt-Schreibweise ist kürzer und im Alltag die erste Wahl. Die eckige Klammer brauchst du in zwei Fällen:

1. Der Schlüssel enthält Sonderzeichen. Ein Punkt funktioniert nur bei Namen aus Buchstaben, Ziffern und _. Sobald ein Doppelpunkt, Bindestrich oder Leerzeichen vorkommt, muss die Klammer her — deshalb siehst du im Handbuch überall die KCForms-Eigenschaften so geschrieben:

var feldDefinition = { "type": "number", "title": "Menge", "kc:column": "menge" };

log.info(feldDefinition.title);            // Menge      — geht
log.info(feldDefinition["kc:column"]);     // menge      — nur so, wegen des ":"

2. Der Schlüsselname steht in einer Variablen. Dann weiß das Skript erst zur Laufzeit, welches Feld gemeint ist:

var bestellung = { kunde: "Anna Berg", betrag: 249.90 };
var welchesFeld = "betrag";

log.info(bestellung[welchesFeld]);   // 249.9
log.info(bestellung.welchesFeld);    // undefined — sucht wörtlich nach "welchesFeld"!

Die zweite Zeile ist ein klassischer Anfängerfehler: Mit dem Punkt sucht das Skript wörtlich nach einem Schlüssel welchesFeld. Den gibt es nicht.


In die Tiefe: Objekte in Objekten

Steckt eine Karte in der Karte, hängst du einfach einen weiteren Punkt an. Lies den Ausdruck von links nach rechts wie eine Wegbeschreibung:

var bestellung = {
  kunde: "Anna Berg",
  adresse: { strasse: "Hafenweg 3", plz: "24103", ort: "Kiel" }
};

log.info(bestellung.adresse.ort);   // Kiel

bestellung → darin das Fach adresse → darin das Fach ort.

Bei einer Liste kommt eine Zahl in eckigen Klammern dazu — die Position. Gezählt wird ab 0:

var bestellung = {
  positionen: [
    { artikel: "Stift", menge: 3 },
    { artikel: "Block", menge: 2 }
  ]
};

log.info(bestellung.positionen[0].artikel);   // Stift  (die erste Zeile)
log.info(bestellung.positionen[1].artikel);   // Block  (die zweite Zeile)

bestellung → Fach positionen → die Zeile Nummer 1 (also die zweite) → darin das Fach artikel.

Gezählt wird ab null

Die erste Zeile ist [0], die zweite [1]. Bei drei Zeilen ist [3] also schon daneben — das gibt undefined. Die letzte Zeile bekommst du immer mit liste[liste.length - 1].


Wenn der Schlüssel gar nicht da ist

Fragst du nach einem Fach, das es nicht gibt, gibt es keinen Fehler — sondern undefined, zu Deutsch „gibt's nicht":

var bestellung = { kunde: "Anna Berg" };

log.info(bestellung.telefon);   // undefined

Unangenehm wird es erst eine Ebene tiefer. bestellung.adresse ist undefined, und in einem Nichts kann man nicht weitersuchen:

var bestellung = { kunde: "Anna Berg" };

log.info(bestellung.adresse.ort);
// Fehlermeldung: Cannot read property 'ort' of undefined

Diese Meldung ist die häufigste im ganzen Skripting. Sie sagt immer dasselbe: Der Teil links vom letzten Punkt war nicht da. Drei Wege, das zu vermeiden:

var bestellung = { kunde: "Anna Berg" };

// 1. Ersatzwert mit "||" — nimm das, sonst jenes
var kunde = bestellung.kunde || "unbekannt";

// 2. Erst fragen, dann zugreifen ("&&" bricht ab, sobald etwas fehlt)
if (bestellung.adresse && bestellung.adresse.ort) {
  log.info("Ort: " + bestellung.adresse.ort);
} else {
  log.info("Ort unbekannt");
}

// 3. Das Fragezeichen: "nur weitergehen, wenn es das gibt"
log.info(bestellung.adresse?.ort);   // undefined statt Absturz

Für Listen ist || [] der Standard-Handgriff: „nimm die Liste, und wenn es keine gibt, eben eine leere". Dann läuft die Schleife danach einfach null Mal, statt zu scheitern.

var bestellung = {};
var zeilen = bestellung.positionen || [];

log.info(zeilen.length + " Zeilen");   // 0 Zeilen

Werte ändern, ergänzen, entfernen

Ein Objekt ist nicht in Stein gemeißelt:

var bestellung = { kunde: "Anna Berg", betrag: 249.90, notiz: "bitte prüfen" };

bestellung.betrag = 199.00;          // ändern
bestellung.status = "gepackt";       // ergänzen — den Schlüssel gab es noch nicht
delete bestellung.notiz;             // entfernen

log.info(bestellung.status);                      // gepackt
log.info("notiz" in bestellung);                  // false — wirklich weg
log.info(JSON.stringify(bestellung));
// {"kunde":"Anna Berg","betrag":199,"status":"gepackt"}

"name" in objekt beantwortet die Frage „gibt es dieses Fach?" mit true oder false. Das ist genauer als ein Vergleich mit undefined, denn ein Fach darf auch absichtlich leer sein (null).


Listen

Eine Liste (Fachwort: Array) steht in eckigen Klammern und hält beliebig viele Werte in fester Reihenfolge:

var artikel = ["Stift", "Block", "Radiergummi"];

log.info(artikel.length + " Artikel");        // 3 Artikel
log.info(artikel[0]);                         // Stift
log.info(artikel[artikel.length - 1]);        // Radiergummi — der letzte
log.info(artikel.join(", "));                 // Stift, Block, Radiergummi

Die wichtigsten Handgriffe:

Handgriff Was er tut
liste.length Wie viele Einträge?
liste[0] Der erste Eintrag
liste.push(x) Hängt x hinten an
liste.join(", ") Macht einen Text aus allen Einträgen
liste.indexOf(x) Position von x, oder -1 wenn nicht vorhanden
liste.slice(0, 5) Die ersten fünf — das Original bleibt unverändert
var gruppen = ["Einkauf", "Buchhaltung"];

gruppen.push("Versand");

if (gruppen.indexOf("Buchhaltung") >= 0) {
  log.info("Buchhaltung ist dabei");
}

Schleifen: jeden Eintrag einmal anfassen

Eine Schleife wiederholt dieselben Anweisungen für jeden Eintrag einer Liste. Die klassische Form nennt einen Zähler beim Namen:

var artikel = ["Stift", "Block", "Radiergummi"];

for (var i = 0; i < artikel.length; i++) {
  log.info((i + 1) + ". " + artikel[i]);
}
// 1. Stift
// 2. Block
// 3. Radiergummi

Die Zeile for (var i = 0; i < artikel.length; i++) liest sich als: Fang bei 0 an; mach weiter, solange i kleiner als die Länge ist; zähle nach jedem Durchgang um eins hoch.

Kürzer und ohne Zähler geht es mit forEach — „für jeden Eintrag tu dies":

var artikel = ["Stift", "Block", "Radiergummi"];

artikel.forEach(function (name) {
  log.info(name);
});

Beide Formen sind gleichwertig. Ein Unterschied zählt im Alltag:

for forEach
Zähler verfügbar ja (i) als zweiter Parameter: function (x, i)
vorzeitig abbrechen ja, mit break nein
Eintrag überspringen continue return
var artikel = ["Stift", "Block", "Radiergummi"];

for (var i = 0; i < artikel.length; i++) {
  if (artikel[i] === "Block") {
    log.info("Gefunden bei " + i);
    break;                       // Schluss, der Rest interessiert nicht mehr
  }
}

Über die Schlüssel eines Objekts laufen

Ein Objekt hat keine Reihenfolge-Nummern, aber Object.keys macht daraus eine Liste seiner Schlüssel — und darüber läuft wieder eine gewöhnliche Schleife:

var bestellung = { kunde: "Anna Berg", betrag: 249.90, bezahlt: false };

Object.keys(bestellung).forEach(function (schluessel) {
  log.info(schluessel + " = " + bestellung[schluessel]);
});
// kunde = Anna Berg
// betrag = 249.9
// bezahlt = false

Beachte die eckige Klammer: Der Name des Fachs steckt ja in der Variablen schluessel. Das ist genau Fall 2 von oben.

Nicht bei Datenbankzeilen

Bei einer Zeile aus db.query(...) liefert Object.keys nicht die Spalten. Dort gilt for (var k in zeile) — warum, steht unter Zeilen aus der Datenbank.


Die vier Handgriffe für Listen von Objekten

Detail-Tabellen, Datenbank-Ergebnisse, Antworten aus dem Internet — sie alle sind Listen von Objekten. Fast alles, was man damit tut, ist eine dieser vier Bewegungen.

Als gemeinsame Grundlage dient diese Positionsliste:

var positionen = [
  { artikel: "Stift",       menge: 3, preis: 1.20 },
  { artikel: "Block",       menge: 2, preis: 2.50 },
  { artikel: "Radiergummi", menge: 5, preis: 0.80 }
];

1. Zusammenzählen — Schleife mit einem Sammler davor:

var positionen = [
  { artikel: "Stift", menge: 3, preis: 1.20 },
  { artikel: "Block", menge: 2, preis: 2.50 },
  { artikel: "Radiergummi", menge: 5, preis: 0.80 }
];

var gesamt = 0;
positionen.forEach(function (p) {
  gesamt += kc.num(p.menge) * kc.num(p.preis);
});

log.info("Gesamt: " + gesamt.toFixed(2));   // Gesamt: 12.60

2. Filtern — filter behält, was die Bedingung erfüllt, und gibt eine neue Liste zurück:

var positionen = [
  { artikel: "Stift", menge: 3, preis: 1.20 },
  { artikel: "Block", menge: 2, preis: 2.50 },
  { artikel: "Radiergummi", menge: 5, preis: 0.80 }
];

var teure = positionen.filter(function (p) {
  return p.preis >= 1.00;
});

log.info(teure.length + " teure Positionen");   // 2 teure Positionen

3. Umbauen — map macht aus jedem Eintrag etwas anderes, ebenfalls als neue Liste:

var positionen = [
  { artikel: "Stift", menge: 3, preis: 1.20 },
  { artikel: "Block", menge: 2, preis: 2.50 },
  { artikel: "Radiergummi", menge: 5, preis: 0.80 }
];

var namen = positionen.map(function (p) {
  return p.artikel;
});

log.info(namen.join(", "));   // Stift, Block, Radiergummi

4. Suchen und sortieren — find liefert den ersten Treffer (oder undefined), sort ordnet:

var positionen = [
  { artikel: "Stift", menge: 3, preis: 1.20 },
  { artikel: "Block", menge: 2, preis: 2.50 },
  { artikel: "Radiergummi", menge: 5, preis: 0.80 }
];

var block = positionen.find(function (p) { return p.artikel === "Block"; });
log.info("Menge Block: " + (block ? block.menge : "nicht gefunden"));

positionen.sort(function (a, b) { return b.preis - a.preis; });   // absteigend
log.info("Teuerste: " + positionen[0].artikel);                   // Teuerste: Block

Die Sortier-Funktion bekommt zwei Einträge und antwortet mit einer Zahl: negativ = a zuerst, positiv = b zuerst. b.preis - a.preis sortiert deshalb absteigend, a.preis - b.preis aufsteigend.

sort verändert die Original-Liste

filter und map geben eine neue Liste zurück und lassen das Original in Ruhe. sort und push ändern die Liste selbst. Sortierst du ctx.form.positionen, steht die Detail-Tabelle danach auch im Formular in dieser Reihenfolge.


Text und Objekt ineinander umwandeln

Ein Objekt lebt im Speicher. Sobald es das Skript verlassen soll — in eine Datei, in ein Textfeld, über das Netz —, muss es Text werden. Und umgekehrt.

Richtung Befehl
Objekt → Text JSON.stringify(objekt)
Text → Objekt JSON.parse(text)
var bestellung = { kunde: "Anna Berg", betrag: 249.90 };

var text = JSON.stringify(bestellung);
log.info(text);            // {"kunde":"Anna Berg","betrag":249.9}
log.info(typeof text);     // string — ab hier ist es nur noch Text

var wieder = JSON.parse(text);
log.info(wieder.kunde);    // Anna Berg — und wieder ein Objekt

Der wichtigste Debug-Handgriff überhaupt

log.info(objekt) schreibt nur [object Object] ins Protokoll — nutzlos. Willst du sehen, was wirklich drinsteht, verpacke es:

log.info(JSON.stringify(objekt));              // alles in einer Zeile
log.info(JSON.stringify(objekt, null, 2));     // eingerückt, gut lesbar

Die 2 ist die Einrücktiefe. Bei unbekannten Antworten aus dem Internet ist das der schnellste Weg herauszufinden, wie sie aufgebaut sind.

Eine Ausnahme gibt es: Bei Datenbankzeilen kommt dabei nur {} heraus — siehe Zeilen aus der Datenbank.

JSON.parse erwartet gültigen JSON-Text und wirft sonst einen Fehler. Bei Daten aus fremder Quelle deshalb absichern:

var antwort = "{ kaputt ohne Anführungszeichen }";

try {
  var daten = JSON.parse(antwort);
  log.info(daten.kunde);
} catch (e) {
  log.warn("Konnte die Antwort nicht lesen: " + e.message);
}

Sonderfall: Zeilen aus der Datenbank

Eine Zeile aus db.queryOne(...) oder db.query(...) sieht aus wie ein Objekt und verhält sich im Alltag auch so — sie ist aber in Wahrheit ein Java-Objekt, das KCForms ins Skript durchreicht. Zwei Dinge funktionieren deshalb anders:

var zeile = db.queryOne("SELECT 'Anna' AS vorname, 42 AS punkte");

log.info(zeile.vorname);             // Anna — normaler Zugriff: geht
log.info(JSON.stringify(zeile));     // {}   ← leer, obwohl Daten drin sind!

var spalten = [];
for (var k in zeile) { spalten.push(k); }
log.info(spalten.join(", "));        // vorname, punkte  ← so bekommst du die Spalten
bei einer Datenbankzeile
zeile.spalte funktioniert wie gewohnt
JSON.stringify(zeile) gibt {} — zum Anschauen unbrauchbar
Object.keys(zeile) liefert nicht die Spalten, sondern interne Java-Methoden
for (var k in zeile) liefert die Spaltennamen — nimm diese Form

Brauchst du eine Zeile als echtes Objekt (zum Ausgeben, Weiterschicken oder Umbauen), schreibe sie einmal um:

function alsObjekt(zeile) {
  var o = {};
  for (var k in zeile) { o[k] = zeile[k]; }
  return o;
}

var zeile = db.queryOne("SELECT 'Anna' AS vorname, 42 AS punkte");
log.info(JSON.stringify(alsObjekt(zeile)));   // {"vorname":"Anna","punkte":42}

Die Liste aus db.query(...) ist dagegen eine ganz normale Liste: length, forEach, filter und map funktionieren wie oben beschrieben — nur die einzelnen Zeilen darin sind eben Datenbankzeilen.


Sieben Stolpersteine

1. Zahlen aus dem Formular sind Text. Der Browser schickt jedes Eingabefeld als Zeichenkette — auch ein Zahlenfeld. + klebt Texte zusammen, statt zu rechnen:

log.info("falsch:  " + (ctx.form.menge + ctx.form.preis));              // 34
log.info("richtig: " + (kc.num(ctx.form.menge) + kc.num(ctx.form.preis))); // 7

Deshalb gehört um jeden Formularwert, mit dem gerechnet wird, ein kc.num(...). Es versteht auch "1.234,56".

2. Ein Objekt ist kein Text. Siehe oben: JSON.stringify beim Ausgeben.

3. Zwei Namen, ein Objekt. Zuweisen kopiert nicht — beide Namen zeigen auf dieselbe Karte:

var a = { betrag: 249.90 };

var b = a;             // KEINE Kopie — b ist derselbe Karteikasten
b.betrag = 1;
log.info("a: " + a.betrag);   // a: 1   ← überrascht!

var c = JSON.parse(JSON.stringify(a));   // echte Kopie, auch in der Tiefe
c.betrag = 249.90;
log.info("Kopie: " + c.betrag + ", Original: " + a.betrag);

4. JSON-Text ist streng. Anders als das Skript verzeiht JSON nichts:

{
  "kunde": "Anna Berg",     ← doppelte Anführungszeichen, keine einfachen
  "betrag": 249.90,         ← Punkt als Dezimaltrenner
  "bezahlt": false          ← KEIN Komma nach dem letzten Eintrag
}

Kommentare sind in JSON ebenfalls nicht erlaubt. Bricht der JSON-Editor im Formular-Editor mit einer Fehlermeldung ab, ist es fast immer eines dieser drei Dinge — meist ein Komma zu viel.

5. null, undefined und "" sind drei verschiedene Dinge. „Fach absichtlich leer", „Fach gibt es nicht" und „leerer Text". In der Formular-Welt willst du meist alle drei gleich behandeln — dafür gibt es kc.isEmpty(...).

6. === statt == vergleichen. == vergleicht großzügig und macht aus Text heimlich Zahlen: "0" == 0 ist true, "" == 0 auch. === vergleicht Wert und Art und liefert keine Überraschungen.

7. Groß- und Kleinschreibung zählt. bestellung.Kunde ist nicht bestellung.kunde. Der falsch geschriebene Zugriff meldet keinen Fehler, sondern liefert still undefined — deshalb lohnt beim Suchen ein log.info(JSON.stringify(objekt)), um die echten Schlüssel zu sehen.


Wo dir JSON in KCForms begegnet

Stelle Was es ist Mehr dazu
Formular-Schema ein großes Objekt mit allen Feldern und kc:-Eigenschaften Grundlagen
ctx.form ein Objekt mit allen Feldwerten des Datensatzes ctx
Detail-Tabellen eine Liste von Objekten unter ctx.form.<name> Detail-Tabellen
db.query(...) eine Liste von Objekten — ein Objekt je Ergebniszeile db
forms.list(...) ebenso: eine Liste von Datensatz-Objekten forms
http.get(...) Antworten aus dem Internet, meist JSON-Text HTTP
REST-Services dein Service bekommt und liefert JSON REST-Services
ctx.model die Beschreibung deines Datenmodells als Objekt ctx.model
Properties, Transport, Export Konfigurationen und Formulare als JSON-Dateien Properties

Ein Beispiel, das drei dieser Stellen verbindet — Detail-Tabelle lesen, umbauen, als Text in ein Feld schreiben:

var zeilen = ctx.form.positionen || [];

var summe = 0;
zeilen.forEach(function (z) {
  summe += kc.num(z.menge) * kc.num(z.preis);
});

ctx.form.betrag = summe;
ctx.form.ergebnis = zeilen.map(function (z) {
  return z.artikel + " (" + z.menge + ")";
}).join(", ");

log.info(ctx.form.ergebnis);   // Stift (3), Block (2)
log.info("Summe: " + summe.toFixed(2));

Spickzettel

Ich will … So geht's
einen Wert lesen objekt.feld oder objekt["feld"]
einen Wert tief drinnen lesen objekt.a.b[0].c
absichern, dass nichts fehlt objekt.a?.b oder objekt.a && objekt.a.b
einen Ersatzwert setzen objekt.feld || "Standard"
einen Wert setzen oder ergänzen objekt.feld = wert
ein Fach entfernen delete objekt.feld
prüfen, ob ein Fach existiert "feld" in objekt
alle Schlüssel durchgehen Object.keys(objekt).forEach(...)
Länge einer Liste liste.length
etwas anhängen liste.push(wert)
jede Zeile durchgehen liste.forEach(function (z) { … })
zusammenzählen Sammler + forEach
aussortieren liste.filter(function (z) { return … })
umbauen liste.map(function (z) { return … })
den ersten Treffer suchen liste.find(function (z) { return … })
sortieren liste.sort(function (a, b) { return a.x - b.x })
ein Objekt anschauen log.info(JSON.stringify(objekt, null, 2))
Objekt → Text JSON.stringify(objekt)
Text → Objekt JSON.parse(text)
echte Kopie JSON.parse(JSON.stringify(objekt))

Weiter