IBM i modernisieren · RPG, Java und verlässliche Geschäftsdaten

Read in English

RPG-Java-Mapping: Sieben Fallen, die Geschäftsdaten verändern

Eine Teillieferung auf dem Weg durch RPG, Db2 for i, Java und JSON: So prüfst du, ob Werte und fachliche Bedeutung erhalten bleiben.

Bei einer Modernisierung fallen zuerst die neuen Oberflächen auf: Aus der Terminalmaske wird eine Webanwendung, ein Java-Service übernimmt Aufgaben der bestehenden Software, eine API verbindet die IBM i mit weiteren Systemen. Für den Betrieb zählt allerdings, dass ein Auftrag weiterhin denselben Kunden meint, zum richtigen Betrag abgerechnet wird und das vorgesehene Lieferdatum behält.

Die Felder wechseln dabei ihre technische Umgebung. Was in RPG und Db2 durch Felddefinitionen, bestehende Programme und gewachsene Fachregeln abgesichert war, muss an der Java-Schnittstelle ausdrücklich festgelegt werden. Sonst läuft die Übertragung fehlerfrei durch, während führende Nullen, Rundungsregeln oder der Unterschied zwischen „noch offen“ und „bereits entschieden“ verloren gehen.

Beim RPG-Java-Mapping brauchen genau diese Details eine klare Regel. An einer einzigen Teillieferung gehen wir sieben typische Fehler durch – jeweils mit einem konkreten Sollwert und Gegenbeispielen, die sich in die eigene Abnahme übernehmen lassen.

Sieben Prüfpunkte zwischen RPG und Java/JSON bewahren die Kundenkennung 00427186 und den Betrag 88.97; Wissen-to-Go-Banner.
KI-generierte Illustration | tiny-tool.de

Eine Lieferung, klare Feldregeln

Eine Werkstatt liefert 2,375 Meter Kühlleitung zum Meterpreis von 37,46 Euro. Nach der vereinbarten Rundungsregel sind dafür 88,97 Euro zu berechnen. Eine kleine Änderung im Konvertierungscode macht daraus 89,15 Euro. Beide Varianten rechnen mit exakten Dezimalzahlen. Keine löst eine Exception aus.

Dieselbe Lieferung kann außerdem die führenden Nullen ihrer Kundenkennung verlieren, einen erfassten Vorgang um eine Stunde verschieben oder aus einer noch offenen Rabattentscheidung einen bestätigten Nullwert machen. Die Java-Typen sehen dabei durchaus passend aus.

Auf IBM i stecken Felddefinitionen häufig in Datenbankspalten, extern beschriebenen Dateien, Datenstrukturen und Programmschnittstellen. Beim Übergang nach Java fallen manche Einschränkungen aus dem Typ heraus: Ein String bringt weder die Breite eines CHAR(8)-Felds noch dessen CCSID mit. Ein BigDecimal übernimmt nicht automatisch die Kapazität eines PACKED(11:2)-Felds.

Für das Beispiel werden die Regeln deshalb bewusst präzise festgelegt. Die Datumswerte sind synthetische Testdaten. Die Beschreibung mit Umlauten prüft den Konvertierungsweg. Zusammen bilden diese Angaben einen durchgängigen Geschäftsdatensatz.

Diese Werte müssen alle Adapter erhalten
Feld Beispielwert Vereinbarte Regel
Länge / Einzelpreis 2.375 / 37.46 Drei beziehungsweise zwei Nachkommastellen; das Produkt nach der vereinbarten Regel auf Cent runden.
Betrag 88.97 DECIMAL(11,2); zwei Nachkommastellen und zulässigen Wertebereich einhalten.
Kundenkennung 00427186 Genau acht ASCII-Ziffern, einschließlich der führenden Nullen.
Lieferdatum 2026-11-12 Ein Kalendertag.
Erfasste lokale Zeit 2026-11-10T16:42:18.735219 Mikrosekunden erhalten; die ausdrücklich vereinbarte Geschäftszeitzone Europe/Berlin anwenden.
Beschreibung Kühlleitung Größe 7 Die Zeichen müssen den geprüften Konvertierungsweg über CCSID 273 unverändert überstehen.
Etikett ␠␠Tor␠C Sieben fachlich relevante Zeichen, davon zwei führende Leerzeichen, in einem zwölf Zeichen langen Speicherfeld. ␠ macht ein gewöhnliches Leerzeichen sichtbar.
Rabatt NULL oder 0.00 Entscheidung offen oder vorhandener Nullwert; nullable DECIMAL(5,2).

Die Codebeispiele verwenden Java-17-Sprachmittel. Für die JSON-Prüfungen kommt Jackson 2.20.0 zum Einsatz, um das untersuchte Verhalten reproduzierbar zu machen. Die Versionsangaben beschreiben den Testaufbau und sind keine Empfehlung für einen historischen Produktivstand. Einige Ausschnitte verzichten auf Imports und den umgebenden Programmaufbau. In Code und JSON bleibt der Dezimalpunkt erhalten.

1. Den falschen Wert runden

Der erste Fehler überlebt sogar den gut gemeinten Rat, für Geldbeträge immer BigDecimal zu verwenden. Die Länge steht in PACKED(7:3), der Preis in PACKED(9:2) und der Betrag in PACKED(11:2). Jedes Feld hat eine andere Aufgabe.

Wer die Länge rundet, verändert die abgerechnete Menge. Wer das Produkt rundet, bringt den berechneten Betrag auf die vereinbarte Anzahl von Nachkommastellen.

Java · Die vereinbarte Berechnung erhalten
BigDecimal quantity = new BigDecimal("2.375");
BigDecimal unitPrice = new BigDecimal("37.46");

// Wrong for this contract: change the quantity first.
BigDecimal wrong = quantity.setScale(2, RoundingMode.HALF_UP)
    .multiply(unitPrice)
    .setScale(2, RoundingMode.HALF_UP);

// Agreed rule: round the product to cents.
BigDecimal correct = quantity.multiply(unitPrice)
    .setScale(2, RoundingMode.HALF_UP);

88,97 €Das Produkt runden

89,15 €Zuerst die Länge runden

0,18 €Unbeabsichtigter Mehrbetrag

Das ungerundete Produkt lautet 88.96750. Mit der vorzeitig gerundeten Menge entsteht dagegen 89.1548. Die Abweichung wird durch die veränderte Eingangsgröße verursacht. Dafür braucht es keinen Fehler durch binäre Gleitkommaapproximation.

Die entsprechende RPG-Operation rundet das Produkt:

RPG · Das Produkt runden
correctAmount = %dech(shippedLength * unitPrice : 11 : 2);

HALF_UP ist die bewusst gewählte Laborregel, auch für negative Werte genau auf einer Rundungsgrenze. Preis-, Steuer- oder Buchungslogik kann andere Vorgaben haben. Kläre diese Vorgaben, bevor du Implementierungen vergleichst. IBM dokumentiert das Halbjustieren in RPG; Java stellt eigene Regeln für Skala, Präzision und Rundung bereit. [1] [2]

Die Feldgrenze gehört an die Schnittstelle

Ein Betrag für DECIMAL(11,2) braucht nach der Berechnung zwei Nachkommastellen und darf höchstens elf Ziffern Präzision haben. Bei dieser festen Skala beträgt der größte zulässige Betrag dem Absolutwert nach 999999999.99. Der lokale Prüfer kontrolliert beide Bedingungen:

Java · Skala und Feldkapazität prüfen
public static BigDecimal requireDecimal(BigDecimal value, int precision, int scale) {
    Objects.requireNonNull(value, "decimal value");
    if (value.scale() != scale || value.precision() > precision) {
        throw new IllegalArgumentException("Value does not fit the decimal contract");
    }
    return value;
}

Die Skala beschreibt hier die Nachkommastellen, die Präzision die Zahl der Ziffern des BigDecimal-Werts. Die Hilfsmethode weist eine falsche Skala ausdrücklich zurück. Sie rundet eingehende Nachrichten nicht stillschweigend passend. Eine Preisberechnung und die Prüfung eines Schnittstellenwerts haben unterschiedliche Aufgaben.

Auch beim Vergleich musst du wissen, was der Test beweisen soll. 88.97 und 88.970 sind mit compareTo numerisch gleich. Ihre Skala unterscheidet sich jedoch; BigDecimal.equals berücksichtigt diesen Unterschied. Welche Gleichheit ausreicht, legt die Feldregel fest. [1]

Abnahmetest: Erwarte ausdrücklich 88.97. Die bloße Übereinstimmung zweier Programme genügt nicht. Bei einem Preis von 37.48 lautet das Ergebnis 89.02, bei der negativen Gegenbuchung -89.02. Prüfe beide Feldgrenzen und die jeweils erste Überschreitung.

Der Datenbankpfad lieferte eine weitere wichtige Beobachtung: Ein einfacher Dezimal-Cast ergab im dokumentierten Db2-Test 88.96. Für den erwarteten Betrag war eine explizite SQL-Rundung nötig. Bei einem separaten Überlauf kamen Warnungen zurück, obwohl der erste Prüfer eine Exception erwartet hatte. Kontrolliere daher auch das Ergebnis und gegebenenfalls die JDBC-Warnungsketten. Eine vermutete Fehlerreaktion ist noch keine nachgewiesene Fehlerbehandlung.

2. Eine Kennung zur Zahl machen

Die Kundenkennung besteht aus Ziffern. Trotzdem ist sie in dieser Schnittstelle keine Rechengröße. Ihre achtstellige Darstellung gehört zur Identität.

Java · Die Kennung bleibt Text
String customer = "00427186";
String wrong = Integer.toString(Integer.parseInt(customer));
String correct = customerReference(customer);

public static String customerReference(String value) {
    if (value == null || !value.matches("[0-9]{8}")) {
        throw new IllegalArgumentException("Customer reference needs eight ASCII digits");
    }
    return value;
}

Der Umweg über den Integer erzeugt 427186. Mathematisch bleibt die ganze Zahl gleich. Die Kennung, die das nächste System erhält, hat sich dagegen verändert.

Verwende [0-9], wenn ausdrücklich ASCII-Ziffern vereinbart sind. Auch ein vorsorgliches trim() vor der Prüfung ist keine neutrale Bereinigung: Sind umgebende Leerzeichen unzulässig, verdeckt ihr stilles Entfernen eine fehlerhafte Eingabe.

Abnahmetest: Prüfe den exakten Inhalt 00427186 und die Länge acht. Weise siebenstellige Werte, umgebende Leerzeichen, Buchstaben und andere Unicode-Ziffernformen zurück. Speichert eine andere Schnittstelle die Kennung numerisch, braucht deren Ausgabe eine eigene Breitenregel.

In einer größeren Anwendung kann ein eigenes Wertobjekt für die Kundenkennung diese Regel bündeln. Die zugrunde liegende Java-Darstellung darf weiterhin ein String sein.

3. Einer lokalen Uhrzeit vorschnell einen Zeitpunkt zuweisen

Das Lieferdatum beantwortet die Frage nach dem Kalendertag. Ein lokaler Zeitstempel hält fest, was die Uhr angezeigt hat. Ein Instant bezeichnet dagegen einen eindeutigen Zeitpunkt auf der Zeitachse. Dafür sind unterschiedliche Informationen nötig.

Im Beispiel bleibt 2026-11-12 ein LocalDate. Der erfasste Zeitstempel bleibt zunächst ein LocalDateTime, bis die vereinbarte Geschäftszeitzone angewendet wird. Wer ihn direkt als UTC behandelt, erhält 2026-11-10T16:42:18.735219Z. Erwartet wird für die Berliner Ortszeit jedoch 2026-11-10T15:42:18.735219Z.

Eine Zeitzone bringt außerdem zwei Sonderfälle mit: Bei der Umstellung im Frühjahr existieren bestimmte lokale Uhrzeiten nicht. Im Herbst kommen andere zweimal vor. Der folgende Adapter weist beide Situationen zurück, statt eine Auflösung stillschweigend auszuwählen:

Java · Genau einen gültigen Offset verlangen
public static Instant uniqueInstant(LocalDateTime local, ZoneId zone) {
    var offsets = zone.getRules().getValidOffsets(local);
    return switch (offsets.size()) {
        case 1 -> local.toInstant(offsets.get(0));
        case 0 -> throw new IllegalArgumentException("Local time does not exist");
        default -> throw new IllegalArgumentException("Local time is ambiguous");
    };
}

getValidOffsets macht diese Fälle unmittelbar prüfbar. Die bequeme Methode LocalDateTime.atZone kann dagegen eine Uhrzeit aus der Frühlingslücke nach vorne verschieben und bei einer doppelten Uhrzeit einen Offset auswählen. Dieses dokumentierte Verhalten kann sinnvoll sein – es muss aber zur fachlichen Regel passen. [3] [4]

Drei Uhrzeiten, drei unterschiedliche Situationen
Lokale Zeit in Europe/Berlin Gültige Offsets Erwartetes Ergebnis
2026-11-10T16:42:18.735219 Einer 2026-11-10T15:42:18.735219Z
2026-03-29T02:30 Keiner Zurückweisen: Die lokale Uhrzeit existiert nicht.
2026-10-25T02:30 Zwei Zurückweisen: Die lokale Uhrzeit ist mehrdeutig.

Diese Zuordnungen gelten in den geprüften Zeitzonendatenbanken. Halte deren tzdb-Version im Testnachweis fest. Soll der genaue Zeitpunkt eines vergangenen Ereignisses dauerhaft erhalten bleiben, speichere zusätzlich den Instant oder einen expliziten Offset. Nur die lokale Uhrzeit und eine Regionskennung zu behalten, lässt die spätere Interpretation von den angewendeten Regeln abhängen.

Der Treiber kann den Wert schon vorher verändert haben

Im untersuchten JTOpen-Aufbau hatte getTimestamp bei JVM-Zeitzone Europe/Berlin die Frühlingslücke bereits auf 03:30 Uhr normalisiert. Ein nachgeschalteter Validator konnte die ursprünglich gespeicherte Uhrzeit 02:30 nicht mehr erkennen.

Der korrigierte Test las die feste Textdarstellung des Treibers und prüfte die lokale Zeit, bevor die Zeitzonenregeln angewendet wurden. Das war eine verifizierte Lösung für diesen Aufbau, keine allgemeine Zusicherung für das Stringformat beliebiger Treiber. Prüfe für deinen Treiber und seine Konfiguration entweder die typisierte JDBC-Abfrage oder das konkret verwendete Textformat.

Abnahmetest: Kontrolliere den erwarteten Instant und alle sechs Nachkommastellen der Sekunde. Nimm Zeitumstellungslücke und doppelte Uhrzeit in den Test auf. Für TIMESTAMP(6) müssen Java-Werte mit feinerer Genauigkeit als Mikrosekunden zurückgewiesen werden; value.getNano() % 1_000 == 0 prüft die Ausrichtung. Die vereinbarte Stellenzahl im Übertragungsformat braucht eine zusätzliche Prüfung.

4. Ersetzte Zeichen hinnehmen

Kühlleitung Größe 7 enthält bewusst Zeichen, die ein Test mit ABC123 nicht abdeckt. Dass der Text auf dem Bildschirm ungefähr lesbar aussieht, reicht nicht. Eine Teilesuche oder ein nachgelagertes Dokument braucht die richtigen Zeichen.

Die bequeme Umwandlung eines Java-Strings in Bytes kann Zeichen ersetzen, die sich im Zielzeichensatz nicht darstellen lassen. Ist das unzulässig, muss der Encoder den Fehler melden:

Java · Zeichenersetzung zurückweisen
Charset charset = Charset.forName("IBM273");
String text = "Kühlleitung Größe 7";

byte[] permissive = text.getBytes(charset);

ByteBuffer strict = charset.newEncoder()
    .onMalformedInput(CodingErrorAction.REPORT)
    .onUnmappableCharacter(CodingErrorAction.REPORT)
    .encode(CharBuffer.wrap(text));

Die Beschreibung lässt sich im lokalen CCSID-273-Test korrekt kodieren. Beim Eurozeichen scheitert der strenge Encoder erwartungsgemäß. Auch ein isoliertes Surrogat wird als ungültige Eingabe zurückgewiesen. Das sind lokale Codec-Prüfungen und kein Nachweis eines fehlgeschlagenen Db2-Schreibvorgangs. Die Fehlerstrategien des Encoders beschreibt die Java-API. [5]

Für die Beschreibung ohne angehängte Füllzeichen werden diese IBM273-Bytes erwartet:

IBM273 · Beschreibung ohne Füllzeichen
D2D08893938589A3A4958740C7996AA18540F7

Dieselbe Bytefolge steht auch im dokumentierten Datenbankergebnis des Rückwegs. Dieser unabhängige Vergleich ist wichtig: Allein das Kodieren und anschließende Dekodieren könnte auch mit einem falsch gewählten Zeichensatz ein in sich stimmiges Ergebnis liefern.

Die Bytes von UTF-8 und EBCDIC dürfen unterschiedlich aussehen. Übereinstimmen muss der dargestellte Text. Für allgemeinere Eingaben kommt eine weitere Unterscheidung hinzu: kodierte Bytes, UTF-16-Codeeinheiten in Java, Unicode-Codepoints und wahrgenommene Zeichen sind verschiedene Längenmaße. String.length() allein beweist deshalb nicht, dass ein beliebiger Text in ein byteorientiertes Feld passt. [6]

Abnahmetest: Prüfe den exakten Unicode-Inhalt und bekannte Speicherbytes für eine festgelegte CCSID. Ergänze ein nicht darstellbares Zeichen und fehlerhafte Eingaben. Wo Ersetzen verboten ist, muss der Test die ausdrückliche Zurückweisung verlangen.

5. Das Layout wegtrimmen

Die zwei führenden Leerzeichen positionieren den Etikettentext. Die fünf Leerzeichen am Ende füllen das Speicherfeld auf. Wer alle sieben für entbehrlich hält, verändert das Druckbild.

Java · Nur vereinbarte Füllzeichen entfernen
String raw = "  Tor C     ";
String wrong = raw.trim();
String correct = logicalLabel(raw);

public static String logicalLabel(String raw) {
    if (raw == null || raw.length() != 12
            || !raw.substring(7).equals("     ")) {
        throw new IllegalArgumentException("Expected seven label characters and five pad blanks");
    }
    return raw.substring(0, 7);
}

trim() liefert Tor C: fünf Zeichen ohne Einrückung. Der feldspezifische Adapter erhält dagegen ␠␠Tor␠C: sieben Zeichen mit Einrückung. Die Symbole ␠ machen die Leerzeichen im Fließtext sichtbar. In den Java-Stringliteralen stehen gewöhnliche U+0020-Leerzeichen.

Der Adapter ist bewusst keine universelle Trim-Hilfsmethode. Er kennt einen fachlich relevanten Teil von sieben Zeichen, auf den exakt fünf Speicherleerzeichen folgen. Ein Feld mit bedeutungsvollen nachgestellten Leerzeichen oder variabler logischer Länge braucht andere Regeln.

Auch SQL-Gleichheit kann einen Darstellungsfehler verdecken. Beim Vergleich unterschiedlich langer Zeichenfolgen arbeitet Db2 for i mit einer temporären Auffüllung auf der rechten Seite. Texte, die sich nur in nachgestellten Leerzeichen unterscheiden, können deshalb gleich sein. Das gilt auch für einen leeren und einen ausschließlich mit Leerzeichen gefüllten String. Ein Gleichheitsvergleich belegt somit weder dieselbe Länge noch dasselbe Layout. [7]

Abnahmetest: Prüfe zwölf gespeicherte und sieben logische Zeichen sowie die beiden führenden Leerzeichen. Wo das Layout zählt, gehören Länge und gegebenenfalls Bytes zum Nachweis. Ein erfolgreicher SQL-Vergleich allein reicht dafür nicht.

6. Eine offene Entscheidung durch den Zahlenwert null ersetzen

Bei diesem Rabattfeld bedeutet SQL-NULL: Die Entscheidung steht noch aus. Ein vorhandener Wert 0.00 bedeutet dagegen: Die Prüfung ist abgeschlossen, es gibt keinen Rabatt. Davon kann abhängen, ob ein Auftrag noch kontrolliert werden muss.

Java / JDBC · Den NULL-Zustand erhalten
// This primitive getter alone cannot distinguish SQL NULL from zero.
double wrong = rs.getDouble("DISCOUNT");

BigDecimal discount = rs.getBigDecimal("DISCOUNT");
boolean pending = rs.wasNull(); // Immediately after the relevant getter.

Ein primitiver double kann keinen Nullverweis aufnehmen. Für SQL-NULL liefert JDBC bei getDouble den Zahlenwert null. Erst wasNull() unterscheidet ihn von einer tatsächlich gespeicherten Null. getBigDecimal liefert eine Objektdarstellung; der unmittelbar folgende wasNull()-Aufruf hält den SQL-Nullzustand fest. Er bezieht sich auf die zuletzt gelesene Spalte. Ein weiterer Getter darf deshalb nicht dazwischenkommen. [8]

Der dokumentierte RPG-Rückweg prüfte den Nullindikator -1 für die offene Entscheidung und 0 für den vorhandenen Zahlenwert null. Ein separater Legacy-Adapter wertet den numerischen Sonderwert -1.00 aus; im modernen Nachrichtenvertrag wird dieser Sentinel zurückgewiesen. Solche Übersetzungen gehören an die Grenze zum Altsystem, statt sich durch die Preislogik zu verteilen.

Für Text gilt derselbe Grundgedanke. Fehlend, leer und mit Leerzeichen gefüllt können unterschiedliche fachliche Zustände sein – selbst dann, wenn ein Datenbankvergleich einige davon gleich behandelt. Die Bedeutung muss pro Feld feststehen.

Abnahmetest: Verwende getrennte Zeilen für „noch offen“ und einen vorhandenen Nullwert. Prüfe Wert und Nullzustand direkt nach dem Lesen. Muss ein Legacy-Sonderwert unterstützt werden, braucht dessen Adapter einen eigenen Nachweis; der moderne Vertrag muss ihn zuverlässig zurückweisen.

7. Dem JSON-Mapper die Vertragsprüfung überlassen

Ein korrektes Java-Objekt schützt den nächsten Verbraucher noch nicht vor Fehlinterpretationen. Das Beispiel legt deshalb eine konkrete JSON-Darstellung fest: Kennung und Betrag werden als Strings übertragen, der lokale Zeitstempel enthält sechs Nachkommastellen, die Geschäftszeitzone steht separat. Der Rabatt ist ausdrücklich null oder ein Dezimalstring.

JSON · Nachricht mit offener Rabattentscheidung
{
  "schemaVersion": 1,
  "customerNumber": "00427186",
  "amount": "88.97",
  "businessDate": "2026-11-12",
  "localTimestamp": "2026-11-10T16:42:18.735219",
  "businessZone": "Europe/Berlin",
  "description": "Kühlleitung Größe 7",
  "logicalLabel": "  Tor C",
  "discount": null
}

Die Strings sind eine bewusste Schnittstellenentscheidung. Daraus folgt nicht, dass JSON-Zahlen grundsätzlich ungenau wären. RFC 8259 erlaubt Implementierungen eigene Grenzen für Zahlenbereich und Präzision. Ein Verbraucher, der einen Betrag durch binären double schleust, kann eine Approximation einführen. Ein Dezimalparser kann dieselbe JSON-Zahl innerhalb seines unterstützten Bereichs exakt erhalten. Dokumentiere deshalb die Darstellung und prüfe den tatsächlichen Verbraucher. [9]

Die JSON-Struktur prüfen, bevor Werte umgewandelt werden

Der ursprüngliche Mapper nahm schemaVersion: 1.9 an und erzeugte daraus den Integer 1. Ein Record-Konstruktor, der anschließend auf Version 1 prüfte, sah nur noch den umgewandelten Wert. Das ungültige JSON-Token war an dieser Stelle bereits verschwunden.

Mit dem untersuchten Standard-Mapper von Jackson 2.20.0 ist dieses Verhalten reproduzierbar. Jackson dokumentiert die Konvertierung von Gleitkommazahlen in Ganzzahltypen über ACCEPT_FLOAT_AS_INT. Das Abschalten der Funktion verhindert diese konkrete Umwandlung. Eine zusätzliche Strukturprüfung formuliert die übrigen Regeln des Übertragungsformats ausdrücklich. [10]

Java · JSON-Typen vor der Objektkonvertierung prüfen
public static ShipmentMessage readMessage(String json) throws IOException {
    JsonNode root = MAPPER.readTree(json);
    requireWireTypes(root);
    return MAPPER.treeToValue(root, ShipmentMessage.class);
}

requireWireTypes ist eine anwendungseigene Hilfsmethode. Sie verlangt zunächst ein JSON-Objekt. Danach wird die Version geprüft, ohne einen Bruchteil abzuschneiden:

Java · Die tatsächliche JSON-Ganzzahl 1 verlangen
JsonNode version = root.get("schemaVersion");
if (version == null || !version.isIntegralNumber()
        || !version.canConvertToInt() || version.intValue() != 1) {
    throw new IllegalArgumentException("schemaVersion must be the JSON integer 1");
}

Für Kennung, Betrag, Datum, lokalen Zeitstempel, Zeitzone, Beschreibung und Etikett werden Strings verlangt. Die Eigenschaft discount muss vorhanden sein und entweder einen String oder JSON-null enthalten. Eine fehlende Eigenschaft und ein ausdrücklich übergebener Nullwert sind unterschiedliche Eingaben.

Danach prüft das Beispiel die Schreibweisen, erzeugt den Record und kontrolliert die Feldregeln: acht ASCII-Ziffern, Dezimalskala und Wertebereich, gültige Kalenderdaten, Mikrosekunden, einen eindeutig auflösbaren Geschäftszeitpunkt, die Etikettenlänge und die Zurückweisung des alten Rabatt-Sentinels. Zielkodierung und Speicherkapazität der Beschreibung prüft weiterhin der Adapter am jeweiligen Zielsystem.

Ein Datumsmuster allein garantiert noch kein gültiges Datum

Die zusätzlichen Veröffentlichungsprüfungen brachten ein weiteres nützliches Gegenbeispiel ans Licht. Mit einem benutzerdefinierten Datumsmuster nahm der untersuchte Mapper den 30. Februar an und normalisierte ihn, statt ihn zurückzuweisen. Die überarbeitete Variante fordert deshalb ausdrücklich striktes Parsen:

Java · Strikt geparste Record-Komponente
@JsonFormat(pattern = "uuuu-MM-dd", lenient = OptBoolean.FALSE)
LocalDate businessDate

Der Zeitstempel verwendet das entsprechende strikte Muster uuuu-MM-dd'T'HH:mm:ss.SSSSSS. Das festgelegte Ausgabeformat erhält sechs Nachkommastellen auch dann, wenn sie auf Nullen enden – etwa bei .735000. Die reine Prüfung auf Mikrosekundengenauigkeit würde diese Schreibweise noch nicht sicherstellen.

Zur Kalenderprüfung kommt der zulässige Jahresbereich. Java erlaubt auch Jahre außerhalb dieses Schnittstellenvertrags. Der zugehörige Record verlangt Jahre von 1 bis 9999 und weist insbesondere das Java-Jahr null zurück. Beachte das tatsächliche RPG-Datumsformat: IBM dokumentiert diesen Bereich für *ISO; Formate mit zweistelliger Jahreszahl haben andere Grenzen. [11]

Der mitgelieferte Reader lehnt außerdem unbekannte Felder, doppelte Eigenschaftsnamen und nachgestellte JSON-Eingaben ab. Das sind ausdrückliche Entscheidungen für diese versionierte Nachricht. Eine Schnittstelle, die additive Erweiterungen zulassen soll, kann eine andere Kompatibilitätsregel wählen – auch diese muss aber gezielt getestet werden.

Abnahmetest: Weise gebrochene oder als String übergebene Versionen, numerische Kennungs- und Betragstokens, Überläufe, ungültige Kalenderdaten, unzulässige Genauigkeit, fehlenden Rabattzustand und den Legacy-Sonderwert zurück. Gültige Nachrichten für offene Entscheidungen und vorhandene Nullwerte bleiben in derselben Testsuite. Strengere Prüfung darf den erlaubten Datenverkehr nicht beschädigen.

Ein Test, der die Daten bis zurück nach RPG begleitet

Der dokumentierte verbundene Systemtest ging die gesamte folgende Strecke:

  1. RPG schrieb zwei synthetische Ausgangszeilen: eine mit offener Rabattentscheidung, eine mit vorhandenem Nullwert.
  2. Java las die Zeilen über JDBC und prüfte die Felddarstellungen.
  3. Die Nachrichten wurden als UTF-8-JSON-Bytes serialisiert und wieder nach Java eingelesen.
  4. Java schrieb separate Rückgabezeilen in Db2 for i.
  5. Ein unabhängiger RPG-Prüfer kontrollierte die zurückgegebenen Lieferwerte und Nullindikatoren.

Betrag, Kundenkennung, Datum, Zeitstempel, Beschreibung, aufgefülltes Etikett und Rabattzustände entsprachen den Sollwerten. Die Rückgabetabelle verwendete VARCHAR(64) für die Beschreibung; die frühere Kodierungsprobe nutzte CHAR(40). Das sind getrennte Speichertests.

Auch ein erfolgreicher Roundtrip kann zueinander passende Fehler verbergen. Ein Adapter entfernt Informationen, ein zweiter ergänzt beim Rückweg einen plausiblen Ersatz. Deshalb muss der letzte Prüfer die erwarteten Werte unabhängig kennen. Zusätzliche Kontrollen der Zwischenwerte zeigen dann, an welchem Übergang etwas verändert wurde.

Sieben Aussagen, die deine Abnahme belegen sollte
Übergang Positivfall Gegenbeispiel
Dezimalberechnung 2.375 × 37.46 → 88.97 Vorzeitiges Runden, negative Halbgrenze, Feldüberlauf.
Kennung 00427186, Länge acht Zahlenkonvertierung, umgebende Leerzeichen, andere Ziffernformen.
Lokale Zeit Erwarteter UTC-Instant und Mikrosekunden UTC-Unterstellung, Zeitlücke, doppelte Uhrzeit, zu feine Genauigkeit.
Zeichenkonvertierung Exakte Beschreibung und bekannte CCSID-Bytes Nicht darstellbares Zeichen, fehlerhafte Eingabe, falscher Zeichensatz.
Etikett Zwei führende Leerzeichen, bekannte logische Länge und Speicherlänge Vollständiges Trimmen, unerwartete Füllzeichen.
Nullable Rabatt Offene Entscheidung bleibt von vorhandenem Nullwert unterscheidbar Primitiver Standardwert, verspätetes wasNull(), Legacy-Sentinel.
JSON-Reader Gültige Nachrichten mit beiden Rabattzuständen bleiben erhalten Automatische Typumwandlung, fehlender Zustand, Überlauf, normalisiertes ungültiges Datum.

Was die Nachweise tatsächlich belegen

Die ursprüngliche Untersuchung verwendete IBM i 7.5, JTOpen 11.2 und lokal unter Windows ausgeführtes Java. Der dokumentierte abschließende Rückweg lief mit Amazon Corretto 17.0.14 und Jackson 2.20.0 einschließlich Java-Time-Unterstützung. Java lief dabei außerhalb der IBM i.

JDBC griff auf Db2-Tabellen zu. Ein davon getrennter direkter ProgramCall tauschte Parameter vom Typ CHAR(8) und PACKED(11:2) aus. Für diesen Aufruf liegen eigene Nachweise zu Bytelayout und CCSID vor. JDBC-Ergebnisse allein hätten seine korrekte Ausführung nicht belegt.

Für die Veröffentlichung wurden die begleitenden Beispiele mit Ziel Java 17 kompiliert und lokal unter Corretto 17.0.20.1+12-LTS, Jackson 2.20.0 und tzdb 2026d ausgeführt. Alle 79 lokalen Prüfungen waren erfolgreich. Sie decken Berechnungen, Kennungen, Zeitauflösung, Codec-Verhalten, Etiketten und die Annahme beziehungsweise Zurückweisung von Nachrichten ab.

Die lokale NULL-Demonstration nutzt ein JDK-CachedRowSet zur Prüfung der JDBC-Getter. Sie ist keine neue JTOpen-Messung. Die oben beschriebenen IBM-i-Ergebnisse stammen aus den aufbewahrten Integrationsnachweisen; der Systemlauf wurde für diese redaktionelle Fassung nicht erneut ausgeführt.

Der Laboraufbau belegt weder HTTP-Verhalten noch produktiven Wiederanlauf, Transaktionsgarantien oder sämtliche möglichen Treibereinstellungen. Dafür braucht es Tests des tatsächlich eingesetzten Betriebswegs.

Checkliste für die Mapping-Abnahme

  1. Die Bedeutung jedes Felds festhalten. Unterscheide Mengen, Kennungen, Geldbeträge, Kalendertage, lokale Uhrzeiten und Entscheidungszustände, bevor du Java-Typen auswählst.
  2. Die Regeln an den Übergängen definieren. Dazu gehören Breite, Präzision, Skala, Rundungszeitpunkt, Kodierung, Füllzeichen, Zeitzonenregel und der Umgang mit fehlenden Werten.
  3. Unabhängige Sollwerte vorgeben. Verwende bekannte Beträge, Kennungen, Instants, Bytefolgen und Nullzustände. Zwei Programme blind miteinander zu vergleichen genügt nicht.
  4. Zu jeder Regel ein Gegenbeispiel aufnehmen. Prüfe negative Werte, Rundungsgrenzen, Feldgrenzen, ungültige Daten, Zeitumstellungen, ungeeignete Zeichen und falsche JSON-Typen.
  5. Die tatsächliche Fehlerreaktion kontrollieren. Exceptions, Warnungen, Zurückweisung und Normalisierung sind unterschiedliche Ergebnisse. Ungültige Werte dürfen nicht unbemerkt zu gültig aussehenden Eingaben werden.
  6. Den realen Rückweg verifizieren. Dokumentiere Laufzeit, Treiber, tzdb, Verbindungsweg und Speicherdefinitionen. Ändert sich eine dieser Grundlagen, gehört der Vertrag erneut auf den Prüfstand.

Eine Typentabelle ist ein guter Anfang. Erst geprüfte Feldregeln zeigen, ob die Geschäftsdaten den Übergang fachlich unverändert überstehen.

Für unsere Lieferung wird die Abnahme damit konkret: Kommen Kundenkennung, Betrag, Zeitstempel, Beschreibung, Etikett und Rabattentscheidung unverändert zurück? Gute Mapping-Tests können genau diese Frage beantworten.

Quellen und weiterführende Artikel

Das durchgängige Beispiel, die Testdarstellung und die redaktionelle Ausarbeitung beruhen auf dem Lieferungs-Laboraufbau des Autors. Die folgenden Primärquellen dokumentieren die verwendeten APIs und Vergleichsregeln. Quellenprüfung für diese Fassung: 17. Oktober 2026.

  1. Java SE 17: BigDecimal – Arithmetik, Skala, Präzision, Rundung und Gleichheit.
  2. IBM ILE RPG: %DECH-Beispiele – Dezimalkonvertierung mit Halbjustierung. Negative Werte und Feldgrenzen der Lieferung sind zusätzlich durch die IBM-i-7.5-Laborprotokolle belegt.
  3. Java SE 17: ZoneRules.getValidOffsets – gültige Offsets, Zeitlücken und mehrdeutige lokale Uhrzeiten.
  4. Java SE 17: LocalDateTime.atZone – Standardauflösung bei Zeitumstellungen.
  5. Java SE 17: CharsetEncoder – fehlerhafte und nicht darstellbare Eingaben.
  6. Java SE 17: String – Kodierung, Trimmen und UTF-16-Indizierung.
  7. IBM Db2 for i 7.5: Zeichenfolgenvergleiche – Auffüllung, nachgestellte Leerzeichen und leere Strings.
  8. Java SE 17: ResultSet – numerische Getter und Bezug von wasNull auf die zuletzt gelesene Spalte.
  9. RFC 8259, Abschnitt 6: Zahlen – Zahlendarstellung und Grenzen einer Implementierung.
  10. FasterXML Jackson 2.20.0: DeserializationFeature – unter anderem ACCEPT_FLOAT_AS_INT. Die weiteren Aussagen zum Reader und zum Datumsparser beruhen auf den reproduzierten lokalen Prüfungen.
  11. IBM ILE RPG 7.5: Datentyp Date – formatabhängige Datumsbereiche, einschließlich des vierstelligen ISO-Jahres.

Den allgemeinen Überblick findest du im Vergleich der Datentypen von RPG und Java. Die grundlegenden Architekturbegriffe erklärt der englische Beitrag IBM RPG vs. Java on IBM i.

Über den Autor: Guido Zeuner ist Softwareentwickler und IT-Berater mit Schwerpunkten in RPG, Java, Db2 for i, APIs und der Modernisierung betrieblicher Anwendungen. Er veröffentlicht Fachartikel auf tiny-tool.de. Bei Recherche, Formulierung, sprachlicher Übertragung und Beispielprüfungen dieser Fassung wurden KI-Werkzeuge unterstützend eingesetzt. Technische Belege sind die benannten Dokumentationen und die protokollierten Testergebnisse.

Read this article in English

Transparenzhinweis


Transparenzhinweis:
Die Inhalte auf tiny-tool.de werden sorgfältig recherchiert, redaktionell geprüft und regelmäßig aktualisiert. Quellen und Zitate werden möglichst nachvollziehbar angegeben. Dennoch übernehmen wir keine Garantie für Richtigkeit, Vollständigkeit oder Aktualität der bereitgestellten Informationen. Irrtümer sind nicht ausgeschlossen.

Redaktion und Einsatz von KI: Bei der Erstellung von Inhalten können digitale Werkzeuge – darunter auch KI-basierte Assistenzsysteme – unterstützend eingesetzt werden, etwa bei Recherche, Strukturierung, sprachlicher Überarbeitung, Übersetzung, Codeanalyse oder visueller Gestaltung. Veröffentlichte Inhalte werden redaktionell geprüft, bearbeitet und von Guido Zeuner freigegeben. Auswahl, Einordnung und Veröffentlichung liegen beim Menschen. KI-Ausgaben gelten nicht als eigenständige Quellen. KI-Systeme sind keine verantwortlichen Autoren oder Redakteure. Weitere Informationen zu Texten, Bildern, Videos und digitalen Personas findest du auf unserer Seite Transparenz beim Einsatz von Künstlicher Intelligenz.

Reichweitenmessung (VG WORT / METIS): Zur Ermittlung der Reichweite einzelner Texte können Zählmarken der VG WORT eingesetzt werden. Im Rahmen der METIS-Zugriffszählung kann eine Client-ID gebildet und ein sogenanntes „METIS Session Cookie“ gesetzt werden. Die Messung dient der statistischen Ermittlung von Textzugriffen und als Grundlage für mögliche Ausschüttungen der VG WORT. Nach Angaben der VG WORT werden dabei keine personenbezogenen Nutzungsprofile erstellt; die Messung dient nicht der Werbung oder dem Marketing-Tracking. Weitere Informationen findest du in unseren Datenschutzhinweisen.

Bitte beachte: Die Inhalte dienen ausschließlich der allgemeinen Information und stellen keine fachliche Beratung dar, insbesondere keine rechtliche, steuerliche, medizinische, technische oder finanzielle Beratung. Die Nutzung der Inhalte erfolgt auf eigene Verantwortung.

Werbung und Affiliate-Links: Einige Beiträge können werbliche Hinweise oder sogenannte Affiliate-Links enthalten. Diese werden entsprechend gekennzeichnet. Beim Klick entstehen dir keine zusätzlichen Kosten; wir erhalten gegebenenfalls eine kleine Provision.

Markenrechtlicher Hinweis: Alle Markennamen, Logos und Produktbezeichnungen sind Eigentum der jeweiligen Rechteinhaber und werden ausschließlich zur Identifikation und Beschreibung verwendet. Eine Verbindung zu den genannten Unternehmen besteht nur, wenn dies ausdrücklich angegeben wird.

Externe Links: Diese Website enthält Verweise auf externe Websites Dritter. Trotz sorgfältiger Prüfung übernehmen wir keine Verantwortung für deren Inhalte. Bei Bekanntwerden rechtswidriger Inhalte werden entsprechende Links geprüft und gegebenenfalls entfernt.