Grundlagen

Datenformate und Serialisierung

Wie ein Objekt aus dem Speicher zu Bytes wird und wieder zurück: JSON, XML und Protobuf im Größenvergleich, warum „Grüße“ manchmal als „Grüße“ ankommt und warum JavaScript manche IDs falsch liest.

Lesestand

Im Arbeitsspeicher ist ein Objekt ein Geflecht aus Feldern und Verweisen. Um es zu speichern oder zu verschicken, muss es zu einer flachen Folge von Bytes werden – das heißt Serialisierung, der Rückweg Deserialisierung. Textformate wie JSON und XML sind lesbar und überall verstanden; Binärformate wie Protobuf sind kleiner und schneller, brauchen aber ein Schema.

Unter jedem Textformat liegt eine zweite Übersetzung: Zeichen werden über Unicode zu Nummern und über eine Kodierung – heute fast immer UTF-8 – zu Bytes. Wer dabei auf beiden Seiten nicht dieselbe Kodierung annimmt, bekommt Zeichensalat.

Von Zeichen zu Bytes

Unicode gibt jedem Zeichen aller Schriften eine Nummer, einen Codepoint: „A“ ist U+0041, „ü“ ist U+00FC, „€“ ist U+20AC, „👋“ ist U+1F44B. Wie diese Nummern als Bytes gespeichert werden, regelt eine Kodierung. UTF-8 braucht für die alten ASCII-Zeichen ein Byte, für Umlaute zwei, für die meisten anderen Schriften drei und für Emoji vier. Tipp etwas ein:

Zeichen, Codepoints, Bytes
1 Byte2 Bytes3 Bytes4 Bytes

Zwei Dinge daraus begegnen Java-Entwicklern ständig:

  • Mojibake – Zeichensalat. Er entsteht, wenn UTF-8-Bytes als ein älterer Zeichensatz wie Windows-1252 oder ISO-8859-1 gelesen werden. Aus den zwei Bytes von „ü“ werden dann zwei Zeichen, „ü“. Die Daten sind nicht kaputt, nur falsch gelesen. Seit Java 18 ist UTF-8 der Standard-Zeichensatz der JVM; vorher hing er vom Betriebssystem ab – eine klassische Fehlerquelle zwischen Windows-Entwicklerrechner und Linux-Server.
  • String.length() zählt keine Zeichen. Java speichert Text intern in UTF-16, und length() zählt dessen 16-Bit-Einheiten. Ein Emoji braucht zwei davon. Wer Text auf „höchstens 10 Zeichen“ kürzt, kann ein Emoji in der Mitte zerschneiden.

Ein Objekt, fünf Formate

Dieselbe Bestellung in den gängigen Formaten. Der Betrag steht absichtlich in Cent als ganze Zahl – dazu gleich mehr.

Formate im Vergleich
FormatStärkenSchwächenTypisch für
JSONeinfach, überall unterstützt, lesbarkeine Kommentare, unscharfe Zahlen, kein DatumstypWeb-APIs, Konfiguration, Dokument-Datenbanken
XMLSchemas (XSD), Namensräume, Attribute, ausgereifte Werkzeugeausführlich, komplex, Sicherheitsfallen beim ParsenBehörden- und Bankformate wie SEPA, Office-Dateien, ältere Enterprise-Schnittstellen
YAMLfür Menschen angenehm, KommentareEinrückung trägt Bedeutung, überraschende TypumwandlungenKonfiguration: Kubernetes, Spring, CI-Pipelines
CSVminimal, jede Tabellenkalkulation kann eskein Standard für Trennzeichen, Escaping und Zeichensatz; flachDatenexporte, Tabellen
Protobufsehr klein und schnell, typisiert, gut weiterentwickelbarnicht lesbar, Schema nötiggRPC, interne Dienste, große Datenmengen

Ähnliche Binärformate sind Avro (beliebt mit Kafka, das Schema reist mit oder liegt in einer Registry), MessagePack und CBOR (binäres JSON ohne Schema).

Die Fallen in JSON

JSON definiert Zahlen ohne Grenzen – aber JavaScript liest jede Zahl als 64-Bit-Gleitkommazahl. Ganze Zahlen sind damit nur bis 253 exakt, also bis 9 007 199 254 740 992. Java-long-IDs gehen bis 263. Hier rechnet dein Browser live:

Die Zahlenfalle

Probier auch 1234567890123456789 oder 0.1 – und eine Zahl unter 9007199254740992.

Deshalb liefern viele APIs große IDs als String. Weitere Fallen:

  • Geld als double: 0,1 ist binär nicht exakt darstellbar (siehe Was ist ein Computer?). Beträge in Cent als ganze Zahl oder als String mit Dezimalstellen, in Java als BigDecimal.
  • Datum: JSON hat keinen Datumstyp. Der Standard ist ein String nach ISO 8601: "2026-10-24T14:30:00Z" – mit Zeitzone oder ausdrücklich in UTC. Zeitstempel als Millisekunden-Zahl sind kompakt, aber unlesbar und anfällig für Verwechslung mit Sekunden.
  • Fehlend oder null: {} und {"rabatt": null} sind zweierlei – „nicht mitgeschickt“ und „ausdrücklich leer“. Bei Teil-Updates mit PATCH ist der Unterschied entscheidend.

Schemas und Weiterentwicklung

Daten leben länger als Code. Eine Nachricht, die Version 1 eines Dienstes schreibt, muss Version 2 noch lesen können – und oft umgekehrt. Dafür gibt es Regeln:

+

Felder hinzufügen

Unproblematisch, wenn alte Leser unbekannte Felder ignorieren und neue Leser mit fehlenden Feldern umgehen können – mit einem Standardwert.

✎

Felder umbenennen

In JSON ein Bruch: Der Name ist der Schlüssel. In Protobuf harmlos: Übertragen wird nur die Feldnummer, der Name existiert nur im Schema.

✗

Bedeutung ändern

Immer ein Bruch: Ein Feld, das bisher Euro enthielt, plötzlich in Cent zu füllen. In Protobuf gilt deshalb: Eine einmal benutzte Feldnummer wird nie wiederverwendet.

Für JSON gibt es mit JSON Schema und OpenAPI ebenfalls Schemas – sie werden aber beim Lesen meist nicht geprüft, sondern dienen der Dokumentation und Codegenerierung.

Für Entwickler übersetzt

record Bestellung(long id, String kunde, long summeCent, boolean versandt, List<Integer> artikel) {}

ObjectMapper json = new ObjectMapper();                          // Jackson
String text = json.writeValueAsString(new Bestellung(42, "Anna", 5990, true, List.of(17, 23)));
Bestellung b = json.readValue(text, Bestellung.class);           // Records gehen direkt

byte[] bytes = text.getBytes(StandardCharsets.UTF_8);             // Zeichensatz immer angeben
"👋".length();                                                    // 2 – UTF-16-Einheiten
"👋".codePointCount(0, 2);                                        // 1 – Zeichen
ThemaWas man wissen sollte
Unbekannte FelderJackson wirft standardmäßig einen Fehler bei Feldern, die die Klasse nicht kennt. Für tolerante Leser: FAIL_ON_UNKNOWN_PROPERTIES abschalten oder @JsonIgnoreProperties(ignoreUnknown = true).
DatumFür LocalDate, Instant und Co. braucht Jackson 2 das Modul jackson-datatype-jsr310; Spring Boot bindet es automatisch ein.
Große Zahlenlong-IDs für JavaScript-Clients mit @JsonFormat(shape = STRING) als String ausliefern.
ZeichensatzÜberall StandardCharsets.UTF_8 angeben – bei getBytes, new String(bytes, …), Readern und Writern. Auch wenn es seit Java 18 der Standard ist: Der Code wird dadurch eindeutig.
java.io.SerializableJavas eingebaute Serialisierung ist bequem, aber gefährlich: Beim Einlesen können manipulierte Daten Code in beliebigen Klassen auf dem Klassenpfad anstoßen – eine der folgenreichsten Sicherheitslücken der Java-Geschichte. Für Daten von außen nie ObjectInputStream verwenden; wenn unvermeidbar, mit Deserialisierungsfiltern absichern.
Protobuf in JavaAus der .proto-Datei erzeugt der Compiler Java-Klassen mit Buildern. Mit gRPC entstehen daraus auch gleich Client und Server.

Häufige Irrtümer

„Unicode und UTF-8 sind dasselbe.“ – Unicode nummeriert die Zeichen, UTF-8 ist eine von mehreren Arten, diese Nummern als Bytes zu schreiben. UTF-16 und UTF-32 sind andere.
„Ein Zeichen ist ein Byte“ – oder ein char. – In UTF-8 sind es 1 bis 4 Bytes, in Javas char 1 oder 2. Und was Menschen als ein Zeichen sehen – etwa eine Flagge oder ein Emoji mit Hautton –, sind oft mehrere Codepoints.
„JSON-Zahlen sind exakt.“ – Im Format ja. Aber jeder Leser entscheidet, wie er sie speichert, und JavaScript rundet ab 253.
„Binärformate sind sicherer, weil unlesbar.“ – Sie sind nur unbequemer zu lesen. Wer die Daten abfängt, dekodiert sie mit einem Werkzeug in Sekunden. Sicherheit kommt von Verschlüsselung, nicht vom Format.
„XML ist tot.“ – In Web-APIs weitgehend abgelöst. Aber jede .docx-Datei ist gezipptes XML, Maven-Projekte beschreiben sich in XML, und europäischer Zahlungsverkehr läuft über XML-Formate.

Verwandte Begriffe