Zum Inhalt springen

5. Semistrukturierte Daten und Dokumentenformate

Zu Zen-Modus wechseln

Relationale Datenbanken speichern strukturierte Daten: feste Tabellen mit festen Spalten und Datentypen. Für den Austausch zwischen Systemen und für flexible Inhalte sind aber Formate verbreitet, die zwar eine Struktur haben, sich aber nicht in ein starres Tabellenschema zwängen lassen. Man nennt sie semistrukturiert. Die beiden wichtigsten sind XML und JSON.

Dieses Kapitel schaut sich beide Formate genauer an: wie sie aufgebaut sind, wie man ihre Korrektheit prüft, wie man sie gezielt abfragt und wie moderne Datenbanken sie direkt speichern.

Daten lassen sich danach einteilen, wie stark ihre Struktur vorgegeben ist:

GradStrukturWo steckt sie?Beispiel
Strukturiertstarr, für alle Datensätze gleichim Schema (getrennt von den Daten)Tabelle customers mit id, name, zip
Semistrukturiertvorhanden, aber flexibelim Dokument selbst (Tags, Schlüssel)JSON- oder XML-Dokument
Unstrukturiertkein festes Mustergar nichtFoto, Video, Fließtext

Der entscheidende Unterschied liegt darin, wo die Struktur beschrieben ist. Bei einer relationalen Tabelle steht das Schema getrennt in der Datenbank; jede Zeile muss sich daran halten. Bei semistrukturierten Daten trägt jedes Dokument seine Struktur selbst mit sich: Ein Kunde hat eine Telefonnummer, der nächste drei, ein dritter keine — und trotzdem ist jedes Dokument für sich lesbar.

Semistrukturierte Formate sind der übliche Weg, Daten zwischen Programmen und über Netzwerke auszutauschen, weil sie für Mensch und Maschine lesbar sind. Jede Web-API, jede Konfigurationsdatei, jeder Datenexport nutzt heute eines dieser Formate.

JSON (JavaScript Object Notation) ist heute das verbreitetste Austauschformat, besonders in Web-APIs. Es entstand aus der Objektschreibweise von JavaScript, ist aber sprachunabhängig: Praktisch jede Programmiersprache kann JSON lesen und schreiben.

JSON kennt genau sechs Wertarten — mehr nicht, und darin liegt seine Einfachheit:

{
"customerId": 42,
"name": "Andrea Hofer",
"active": true,
"discount": null,
"phone": ["0463 123456", "0664 987654"],
"address": {
"zip": "9500",
"city": "Villach"
}
}
TypSchreibweiseBeispiel
Objekt{ } mit Schlüssel-Wert-Paaren{ "zip": "9500" }
Array[ ] mit geordneter Liste["a", "b", "c"]
Stringin doppelten Anführungszeichen"Villach"
NumberGanz- oder Kommazahl42, 3.14, -1e6
BooleanWahrheitswerttrue, false
Null„kein Wert”null

Auffällig gegenüber einer Tabelle: Werte können verschachtelt sein (die Adresse ist ein Objekt im Objekt) und mehrfach vorkommen (mehrere Telefonnummern in einem Array). Genau das lässt sich relational nur über mehrere Tabellen abbilden.

JSON ist streng. Wenige Regeln, aber sie werden konsequent durchgesetzt:

  • Schlüssel und Strings müssen in doppelten Anführungszeichen stehen — einfache ('…') sind nicht erlaubt.
  • Kein Komma nach dem letzten Element eines Objekts oder Arrays (kein trailing comma).
  • Keine Kommentare — JSON kennt sie nicht (anders als JavaScript).
  • Zahlen ohne führende Null und ohne +; der Dezimaltrenner ist immer der Punkt.
// FALSCH – so sieht ein häufiger Fehler aus:
{
'name': "Andrea", // einfache Anführungszeichen
"tags": ["a", "b",], // trailing comma
}

In JavaScript ist JSON direkt eingebaut. Zwei Funktionen genügen für den Weg hin und zurück:

// String -> Objekt (parsen / deserialisieren)
const data = JSON.parse('{ "name": "Andrea", "active": true }');
console.log(data.name); // "Andrea"
// Objekt -> String (serialisieren)
const text = JSON.stringify(data, null, 2); // 2 = Einrückung für Lesbarkeit

Das Umwandeln eines Objekts in einen String heißt Serialisierung, der umgekehrte Weg Deserialisierung (oder Parsen). Über das Netzwerk werden immer Strings übertragen; die Struktur entsteht erst beim Parsen wieder.

XML (Extensible Markup Language) ist älter, ausführlicher und stark im Unternehmens- und Dokumentenumfeld verankert. Dieselben Daten wie oben:

<?xml version="1.0" encoding="UTF-8"?>
<customer id="42" active="true">
<name>Andrea Hofer</name>
<phone>0463 123456</phone>
<phone>0664 987654</phone>
<address>
<zip>9500</zip>
<city>Villach</city>
</address>
</customer>

XML unterscheidet zwischen Elementen und Attributen:

  • Ein Element ist alles zwischen einem Start-Tag und einem End-Tag: <name>Andrea Hofer</name>. Elemente können Text oder weitere Elemente enthalten (Verschachtelung).
  • Ein Attribut steht im Start-Tag und beschreibt das Element näher: id="42". Attributwerte stehen immer in Anführungszeichen.
  • Ein leeres Element kann man kurz schreiben: <phone/> statt <phone></phone>.
  • Die erste Zeile ist der Prolog (<?xml … ?>). Er nennt die XML-Version und die Zeichenkodierung.

Es gibt genau ein Wurzelelement, das alles andere umschließt (hier <customer>). Fehlt es oder gibt es mehrere, ist das Dokument kaputt.

Eine wiederkehrende Frage beim Entwurf: Gehört eine Angabe in ein Element oder in ein Attribut? Eine bewährte Faustregel:

  • Attribute für Metadaten, die das Element beschreiben (eine ID, ein Zustand, eine Einheit).
  • Elemente für die eigentlichen Daten, besonders wenn sie selbst Struktur haben oder mehrfach vorkommen können.

Ein Attribut kann pro Element nur einmal vorkommen und keine Kindstruktur tragen — deshalb wurden die Telefonnummern oben als wiederholte Elemente modelliert, nicht als Attribut.

XML hat einige Sonderfälle, die man kennen sollte:

<!-- Ein Kommentar. Anders als JSON kennt XML Kommentare. -->
<note priority="high">
<text>5 &lt; 10 und Tom &amp; Jerry</text>
<script><![CDATA[ if (a < b && b > c) { … } ]]></script>
</note>
  • Entitäten ersetzen Sonderzeichen, die sonst als Markup gälten: &lt; für <, &gt; für >, &amp; für &, &quot; für ", &apos; für '.
  • Ein CDATA-Abschnitt (<![CDATA[ … ]]>) schützt einen Textblock komplett vor der Interpretation — praktisch für eingebetteten Code, der viele Sonderzeichen enthält.

Werden XML-Dokumente aus verschiedenen Quellen zusammengeführt, können Elementnamen kollidieren (zweimal <title> mit anderer Bedeutung). Namensräume (namespaces) lösen das über ein Präfix, das an eine eindeutige URI gebunden ist:

<root xmlns:book="https://example.org/book"
xmlns:html="http://www.w3.org/1999/xhtml">
<book:title>Datenbanken</book:title>
<html:title>Seitentitel</html:title>
</root>

XML ist damit ausdrucksstärker als JSON, aber auch geschwätziger: Dieselbe Information braucht spürbar mehr Zeichen.

Bei XML unterscheidet man zwei Stufen von Korrektheit:

  1. Wohlgeformt (well-formed): Die reine Syntax stimmt — genau ein Wurzelelement, alle Tags korrekt verschachtelt und geschlossen, Attributwerte in Anführungszeichen, Sonderzeichen als Entitäten. Ein Parser kann das Dokument überhaupt einlesen.

  2. Valide (valid): Zusätzlich hält sich das Dokument an ein Schema, das vorschreibt, welche Elemente in welcher Reihenfolge und mit welchen Typen erlaubt sind.

Ein Dokument kann wohlgeformt sein, ohne valide zu sein (die Syntax stimmt, aber es steht ein unerlaubtes Element drin). Umgekehrt geht nicht: Valide setzt Wohlgeformtheit voraus.

SpracheFormatFürAusdruckskraft
DTDeigene SyntaxXMLeinfach, keine Datentypen
XSD (XML Schema)selbst XMLXMLmächtig, mit Typen und Einschränkungen
JSON Schemaselbst JSONJSONmächtig, mit Typen und Einschränkungen

Ein XSD legt für XML zum Beispiel fest, dass <zip> genau vier Ziffern hat und <customer> ein Pflicht-Attribut id braucht. Bemerkenswert dabei: Ein XSD ist selbst wieder XML — die Regeln stehen also im selben Format wie die Daten, die sie beschreiben.

Für JSON gibt es das direkte Gegenstück, JSON Schema. Auch hier gilt dasselbe Prinzip: Das Schema ist selbst wieder ein JSON-Dokument. Das folgende Beispiel beschreibt nicht einen konkreten Kunden, sondern die Regeln, denen jedes Kundenobjekt genügen muss — welche Felder Pflicht sind (required), welchen Typ sie haben und welche Einschränkungen gelten:

{
"type": "object",
"required": ["customerId", "name"],
"properties": {
"customerId": { "type": "integer" },
"name": { "type": "string", "minLength": 1 },
"active": { "type": "boolean" },
"phone": { "type": "array", "items": { "type": "string" } }
}
}

Damit lässt sich vor der Weiterverarbeitung sicherstellen, dass ein empfangenes Dokument die erwartete Struktur hat — ein Kunde ohne name wird zurückgewiesen, bevor er Schaden anrichtet. In Web-APIs übernimmt oft ein Validierungswerkzeug (etwa Ajv in JavaScript) diese Prüfung automatisch.

Beide Formate lassen sich gezielt abfragen, ohne alles einzulesen.

Als durchgehendes Beispiel für die nächsten Abschnitte dient ein kleiner Buchladen — einmal als XML, einmal als JSON. So lassen sich dieselben Fragen in beiden Welten stellen.

<?xml version="1.0" encoding="UTF-8"?>
<bookstore>
<book category="db" lang="de">
<title>Datenbanken</title>
<author>Anna Berg</author>
<author>Tom Klein</author>
<year>2021</year>
<price>39.90</price>
</book>
<book category="web" lang="en">
<title>Learning Astro</title>
<author>Sara Vogt</author>
<year>2024</year>
<price>29.50</price>
</book>
<book category="db" lang="en">
<title>NoSQL Distilled</title>
<author>Mike Ford</author>
<year>2019</year>
<price>24.00</price>
</book>
</bookstore>

XPath (XML Path Language) beschreibt einen Pfad durch die Baumstruktur eines XML-Dokuments, ganz ähnlich wie ein Pfad durch Verzeichnisse. Das Ergebnis einer XPath-Abfrage ist immer eine Menge von Knoten (Elemente, Attribute, Textinhalte) oder ein einfacher Wert (Zahl, Text, Wahrheitswert).

XPath sieht das Dokument als Baum aus Knoten: das Dokument selbst, Elemente, Attribute und Textknoten. Ein Pfad wird aus Schritten aufgebaut, getrennt durch /:

/bookstore/book/title -> alle <title> unterhalb von /bookstore/book (absoluter Pfad)
book/title -> relativ zum aktuellen Knoten
/bookstore/book[1]/title -> nur der Titel des ersten Buchs
AusdruckBedeutung
/Wurzel — ein Schritt vom Dokumentanfang aus
//beliebig tief — überspringt Zwischenebenen
.der aktuelle Knoten
..der Elternknoten (eine Ebene höher)
@ein Attribut statt eines Elements
*ein beliebiges Element (Platzhalter)
@*ein beliebiges Attribut
text()der Textinhalt eines Elements
//title -> alle <title> im ganzen Dokument, egal wie tief
//book/@category -> das category-Attribut jedes Buchs
//book/* -> alle Kindelemente jedes Buchs
//title/text() -> die reinen Textinhalte der Titel

In eckigen Klammern steht eine Bedingung (ein Prädikat). Nur Knoten, die sie erfüllen, bleiben übrig. Das ist das Herzstück von XPath:

//book[@category='db'] -> alle Bücher der Kategorie db
//book[price > 30] -> alle Bücher teurer als 30
//book[year < 2020] -> alle vor 2020 erschienenen Bücher
//book[author='Anna Berg'] -> Bücher mit diesem Autor
//book[@lang='en' and price < 25] -> Englisch UND billiger als 25
//book[2] -> das zweite Buch (Position!)
//book[last()] -> das letzte Buch
//book[price][@lang='de'] -> hat einen Preis UND ist Deutsch

Zahlen in Klammern sind Positionen ([1] ist das erste, [last()] das letzte). XPath zählt ab 1, nicht ab 0.

XPath bringt eine kleine Bibliothek von Funktionen mit, die in Prädikaten oder als Ergebnis nützlich sind:

count(//book) -> Anzahl der Bücher (hier 3)
sum(//book/price) -> Summe aller Preise
//book[contains(title,'Astro')] -> Titel enthält "Astro"
//book[starts-with(@category,'d')]-> Kategorie beginnt mit "d"
//author[position()=1] -> jeweils der erste Autor
string-length(//title[1]) -> Länge des ersten Titels

Häufig gebraucht: count(), sum(), contains(), starts-with(), not(), position(), last(), normalize-space() (entfernt überflüssige Leerzeichen).

Bisher ging es immer nach unten (zu Kindern) oder zu Attributen. XPath kann aber in jede Richtung durch den Baum gehen — das leisten die Achsen. Die volle Schreibweise lautet achse::knotentest[prädikat]:

child::title -> Kindelemente <title> (Standard, meist weggelassen)
descendant::author -> alle Nachfahren <author>, beliebig tief
parent::* -> der Elternknoten (Kurzform: ..)
ancestor::book -> das umschließende <book>
following-sibling::author -> nachfolgende Geschwister <author>
attribute::category -> das Attribut category (Kurzform: @category)

Die Kurzschreibweisen (//, .., @) sind nur bequeme Abkürzungen für die häufigsten Achsen. Für alles andere greift man zur vollen Form.

XSLT (Extensible Stylesheet Language Transformations) ist eine eigene Sprache, um ein XML-Dokument in ein anderes Format umzuwandeln — typischerweise in HTML für die Anzeige, aber auch in reines Text-, CSV- oder anderes XML. Ein XSLT-Stylesheet ist selbst wieder XML.

Das Grundprinzip: Ein Stylesheet besteht aus Templates. Jedes Template matcht bestimmte Knoten (per XPath) und beschreibt, was für sie ausgegeben wird. Der Prozessor läuft den Baum ab und wendet auf jeden Knoten das passende Template an.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- Entry point: matches the document root -->
<xsl:template match="/">
<html>
<body>
<h1>Buchliste</h1>
<table>
<tr><th>Titel</th><th>Jahr</th><th>Preis</th></tr>
<!-- Loop over all books, sorted by price -->
<xsl:for-each select="bookstore/book">
<xsl:sort select="price" data-type="number"/>
<tr>
<td><xsl:value-of select="title"/></td>
<td><xsl:value-of select="year"/></td>
<td><xsl:value-of select="price"/> &#8364;</td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>

Die wichtigsten XSLT-Bausteine:

ElementAufgabe
<xsl:template match="…">Regel für bestimmte Knoten (XPath)
<xsl:value-of select="…">den Wert eines Knotens ausgeben
<xsl:for-each select="…">über eine Knotenmenge schleifen
<xsl:sort select="…">die Schleife sortieren
<xsl:if test="…">bedingte Ausgabe
<xsl:choose> / <xsl:when>mehrere Fälle (wie switch)
<xsl:apply-templates/>passende Templates auf die Kinder anwenden

Mit <xsl:if> und <xsl:choose> lässt sich die Ausgabe steuern — etwa nur teure Bücher hervorheben:

<xsl:for-each select="bookstore/book">
<xsl:if test="price &gt; 30">
<li class="teuer"><xsl:value-of select="title"/></li>
</xsl:if>
</xsl:for-each>

JSONPath ist die Idee von XPath, übertragen auf JSON. Statt / trennt ein Punkt die Schritte, statt // gibt es .., und die Wurzel heißt $. Das Ergebnis ist wieder eine Liste von Treffern.

$ -> die Wurzel (das ganze Dokument)
$.bookstore.book -> das book-Array
$.bookstore.book[0] -> das erste Buch (JSONPath zählt ab 0!)
$.bookstore.book[0].title -> dessen Titel
$.bookstore.book[-1] -> das letzte Buch
AusdruckBedeutung
$Wurzelelement
.name oder ['name']ein Feld (die zweite Form erlaubt Sonderzeichen)
[n]das n-te Array-Element (ab 0)
[*]alle Elemente eines Arrays
..namerekursiv: name in beliebiger Tiefe
*beliebiges Feld / Element
$.bookstore.book[*].title -> die Titel aller Bücher
$..title -> alle "title" irgendwo im Dokument
$..author -> jeder "author", egal wie tief verschachtelt

Wie in Python lassen sich Ausschnitte eines Arrays herausschneiden, und in eckigen Klammern mit ?( … ) steht ein Filter — das Gegenstück zu XPaths Prädikat. Innerhalb des Filters bezeichnet @ das gerade geprüfte Element:

$.bookstore.book[0:2] -> die ersten zwei Bücher (Slice)
$.bookstore.book[?(@.price > 30)] -> alle Bücher teurer als 30
$.bookstore.book[?(@.category == 'db')] -> alle db-Bücher
$.bookstore.book[?(@.year < 2020)].title -> Titel der Bücher vor 2020
$.bookstore.book[?(@.lang == 'en' && @.price < 25)]
Alle Titel
Titel des ersten Buchs
Bücher teurer als 30
Alle Autoren, beliebig tief
Das letzte Buch

Die Denkweise ist dieselbe, nur die Schreibweise unterscheidet sich — und zwei Stolpersteine: JSONPath zählt Arrays ab 0, XPath zählt Positionen ab 1; und JSONPath ist (anders als XPath) kein offizieller Standard, sodass exotische Ausdrücke je nach Bibliothek leicht abweichen können.

Für JSON auf der Shell hat sich jq durchgesetzt — mächtiger als reines JSONPath, mit Pipes wie in Unix. Werte fließen von links nach rechts durch mehrere Stufen:

Terminal-Fenster
# Die Titel aller Bücher
jq '.bookstore.book[].title' books.json
# Nur db-Bücher, davon Titel und Preis als neues Objekt
jq '.bookstore.book[] | select(.category == "db") | {title, price}' books.json
# Nach Preis sortieren und den Durchschnitt berechnen
jq '[.bookstore.book[].price] | add / length' books.json

.[] packt ein Array aus, select( … ) filtert, {title, price} baut ein neues Objekt, add/length rechnen. Damit lässt sich JSON auf der Kommandozeile fast wie mit SQL auswerten — ideal für schnelle Auswertungen von API-Antworten.

Semistrukturierte Daten in der relationalen Datenbank

Abschnitt betitelt „Semistrukturierte Daten in der relationalen Datenbank“

Wichtig ist auch: Moderne relationale Datenbanken können semistrukturierte Daten selbst speichern und abfragen. So muss man sich nicht zwischen den Welten entscheiden.

PostgreSQL hat den Datentyp jsonb (binär gespeichertes JSON) und eigene Operatoren dafür:

-- All customers from Villach; address is stored as a jsonb column
SELECT name
FROM customers
WHERE address ->> 'city' = 'Villach';
-- Reach into a nested field and index it for speed
SELECT name FROM customers WHERE address -> 'geo' ->> 'zone' = 'A';
CREATE INDEX idx_city ON customers ((address ->> 'city'));
OperatorBedeutungErgebnis
->Feld als JSONaddress -> 'city'"Villach" (JSON)
->>Feld als Textaddress ->> 'city'Villach (Text)
@>enthälttags @> '["web"]'

So verbindet man die Stärke der relationalen Welt (Transaktionen, Joins, Constraints) mit der Flexibilität semistrukturierter Daten. Auch für XML gibt es in vielen Datenbanken einen eigenen xml-Typ samt XPath-Unterstützung.

XML und JSON sind die wichtigsten, aber nicht die einzigen:

  • YAML — gut lesbar, ohne Klammern, mit Einrückung als Struktur. Verbreitet für Konfigurationsdateien (z. B. Docker, CI-Pipelines).
  • CSV — kommagetrennte Werte, minimal und tabellarisch, aber nur für flache Daten.
  • TOML — Konfigurationsformat, ein Kompromiss aus Lesbarkeit und Eindeutigkeit.
  • Protocol Buffers / Avro — binäre, schemagebundene Formate für sehr großen Durchsatz zwischen Diensten.
# Dieselben Daten in YAML
customerId: 42
name: Andrea Hofer
active: true
phone:
- 0463 123456
- 0664 987654

Wer fremde Dokumente einliest, öffnet eine Angriffsfläche. Zwei klassische Fallen:

  • XXE (XML External Entity): XML kann über Entitäten externe Ressourcen nachladen. Ein bösartiges Dokument kann so Dateien vom Server auslesen. Gegenmittel: externe Entitäten im Parser abschalten.
  • Unkontrolliertes Parsen: Sehr tief verschachtelte oder riesige Dokumente können den Parser überlasten. Grenzen setzen und die Größe prüfen.
  • JSON für Web-APIs, Konfigurationsdateien und die Kommunikation zwischen modernen Anwendungen. Kompakt, direkt an die meisten Programmiersprachen anschlussfähig, für Menschen gut lesbar.
  • XML dort, wo etablierte Standards, strenge Schemata, Namensräume oder gemischte Text-/Markup-Inhalte es verlangen (etwa Rechnungsformate wie ebInterface/ZUGFeRD, Behördenschnittstellen, Dokumentenaustausch).

Die Wahl folgt der Anforderung, nicht der Mode — genau wie bei relational gegen NoSQL.

  • Semistrukturierte Daten tragen ihre Struktur im Dokument selbst und sind flexibler als eine feste Tabelle; sie liegen zwischen strukturiert (Tabelle) und unstrukturiert (Foto, Fließtext).
  • JSON ist das kompakte Standardformat für Web-APIs mit sechs Wertarten (Objekt, Array, String, Number, Boolean, Null); die Syntax ist streng (doppelte Anführungszeichen, kein trailing comma, keine Kommentare).
  • XML ist ausführlicher und unterscheidet Elemente, Attribute, Entitäten, CDATA und Namensräume; es hat genau ein Wurzelelement.
  • Wohlgeformtheit betrifft die Syntax, Validität die Übereinstimmung mit einem Schema (DTD, XSD, JSON Schema).
  • Abgefragt wird XML mit XPath/XSLT, JSON mit JSONPath/jq oder direkt in der Programmiersprache; Serialisierung und Deserialisierung wandeln zwischen String und Objekt.
  • Relationale Datenbanken speichern semistrukturierte Daten auch selbst — PostgreSQL etwa als jsonb mit eigenen Operatoren (->, ->>, @>).
  • Empfangene Dokumente werden erst validiert, dann verarbeitet — auch aus Sicherheitsgründen (Stichwort XXE).