ABAP Cloud - JSON Konvertierung
Gibt es eine neue API zur Konvertierung von JSON und benötigst du diese für ABAP Cloud? Hier beleuchten wir einmal den Aufbau und die Konvertierung von JSON.
Inhaltsverzeichnis
Wie ist JSON aufgebaut und wie kannst du leicht damit in ABAP arbeiten? In diesem Artikel werden wir diesen und verschiedenen anderen Fragen nachgehen. Dabei werden wir uns das Format und die Nutzung im ABAP Bereich anschauen.
Einleitung
Für die Nutzung von REST-basierten Schnittstellen gehört JSON mittlerweile zum Standard in der Verarbeitung und Datenbereitstellung. Ebenso setzt OData v4 voll auf das Format JSON, da es leichtgewichtig ist und im Gegensatz zu XML nicht so viel Speicher benötigt. JSON Formate lassen sich recht einfach in Datenobjekte konvertieren und sind leicht lesbar, was es zum Tool der Wahl macht.
Grundlagen
Wie ist nun eigentlich ein JSON aufgebaut und wie kannst du es einfach lesen und in ABAP Strukturen umwandeln? In diesem Abschnitt schauen wir uns die verschiedenen Elemente an.
Aufbau
Schauen wir uns dazu erst einmal ein erstes Beispiel an. Dazu haben wir ein JSON aufgebaut, welches verschiedene Abschnitte enthält.
{
"text": "My text",
"number_integer": 37,
"number_decimal": 10.12,
"boolean": true,
"array_element": [
{
"text2": "A",
"number2": 1
},
{
"text2": "B",
"number2": 2
},
{
"text2": "C",
"number2": 3
}
],
"array_data": [
"A-A",
"A-B",
"B-A"
],
"dynamic_list": {
"AED": 12.50,
"EUR": 5.20,
"USD": 9.96
}
}
Dazu nun die folgenden Fakten:
- Bei { } handelt es sich um ein Objekt, dieses Element ist vergleichbar mit einer Struktur in ABAP. Innerhalb des Objektes gibt es verschiedene Felder, die du über den Namen ansprechen kannst. Jeder Name muss eindeutig sein (Schlüssel).
- Bei [ ] handelt es sich um ein Array, also eine Tabelle in ABAP. Ein Array kann einfach sein wie zum Beispiel der Knoten "array_data" oder eine Struktur in Form von Objekten haben ("array_element").
- Der Name eines Feldes wird mit " " abgegrenzt, der Datentyp wird durch die Schreibweise beschrieben.
- String - "Ein Text als Wert"
- Zahl - 5.20 oder 12
- Boolean - true oder false
- Nach dem letzten Element eines Arrays oder Objekts folgt kein Komma mehr.
JSON Struktur
Wenn wir Daten von JSON nach ABAP konvertieren, haben wir die Möglichkeit dynamisch mit den Daten zu arbeiten und Referenzen zu verwenden. Allerdings können wir auch eine lokale Struktur erstellen, um die Daten direkt in eine ABAP Struktur zu mappen und so einfach zugreifen zu können. Über ein Onlinetool wie den JSON Editor Online, kannst du dir ein JSON anzeigen und aufbereiten lassen. Vor allem wenn die Formatierung einmal nicht so einfach ist, übernimmt das Tool das für dich und du kannst das JSON leichter lesen. Beginnen wir daher mit dem Aufbau der Struktur von außen nach Innen und im ABAP von unten nach oben:
Da der äußere Teil ein Objekt { } ist, beginnen wir mit der Anlage einer Struktur und können die verschiedenen Elemente übernehmen. Für DECIMAL legen wir einen lokalen Datentypen an, die Felder der Struktur übernehmen wir 1:1 aus den Daten. Für die Arrays [ ] legen wir schon einmal Tabellentypen an, die wir im Nachgang erzeugen, für dynamic_list setzen wir eine Referenz, da das Element keine feste Struktur besitzt, sondern das Mapping Währung auf Betrag.
TYPES td_decimal TYPE p LENGTH 16 DECIMALS 2.
TYPES: BEGIN OF ts_dummy_data,
text TYPE string,
number_integer TYPE i,
number_decimal TYPE td_decimal,
boolean TYPE abap_bool,
array_element TYPE tt_element,
array_data TYPE tt_data,
dynamic_list TYPE REF TO data,
END OF ts_dummy_data.
Legen wir nun das nächste Element an, in diesem Fall eine Tabelle die aus Objekten, also Strukturen, besteht.
Da das Objekt so weit "stabil" aussieht, können wir dafür eine Struktur anlegen, die zwei Felder hat. Dazu legen wir noch den Tabellentypen an, den wir bereits in unserer Datenstruktur verwendet haben. Die Definition sieht dann wie folgt aus:
TYPES: BEGIN OF ts_element,
text2 TYPE string,
number2 TYPE i,
END OF ts_element.
TYPES tt_element TYPE STANDARD TABLE OF ts_element WITH EMPTY KEY.
Im JSON handelt es sich um ein Array [ ], da aber kein Objekt { } enthalten ist, handelt es sich um eine Tabelle mit einfachem Datentyp.
Wir generieren als zuerst einmal einen einfachen Datentyp. Sind wir uns sicher, dass die Werte kurz sind, können wir direkt CHAR mit einer entsprechenden Länge verwenden, ansonsten würden wir auf String zurückgreifen.
TYPES td_char TYPE c LENGTH 10.
TYPES tt_data TYPE STANDARD TABLE OF td_char WITH EMPTY KEY.
Der finale Typ des JSONs sieht nun wie folgt aus, dabei musst du bei der Anlage der einzelnen Typen auf die Reihenfolge der Anlage achten:
TYPES td_decimal TYPE p LENGTH 16 DECIMALS 2.
TYPES td_char TYPE c LENGTH 10.
TYPES: BEGIN OF ts_element,
text2 TYPE string,
number2 TYPE i,
END OF ts_element.
TYPES tt_element TYPE STANDARD TABLE OF ts_element WITH EMPTY KEY.
TYPES tt_data TYPE STANDARD TABLE OF td_char WITH EMPTY KEY.
TYPES: BEGIN OF ts_dummy_data,
text TYPE string,
number_integer TYPE i,
number_decimal TYPE td_decimal,
boolean TYPE abap_bool,
array_element TYPE tt_element,
array_data TYPE tt_data,
dynamic_list TYPE REF TO data,
END OF ts_dummy_data.
ABAP Struktur
Für die Konvertierung von ABAP nach JSON benötigen ebenfalls eine Struktur mit verschiedenen Typen, um aus ABAP heraus JSON zu erzeugen. Dafür legen wir eine einfache Struktur an, die ein Objekt erzeugen soll.
TYPES: BEGIN OF ts_internal,
camel_string TYPE string,
singlestring TYPE string,
camel_number TYPE td_decimal,
singlenumber TYPE td_decimal,
camel_int TYPE i,
singleint TYPE i,
camel_bool TYPE abap_bool,
singlebool TYPE abap_bool,
END OF ts_internal.
TYPES tt_internal TYPE STANDARD TABLE OF ts_internal WITH EMPTY KEY.
Klasse /UI2/CL_JSON
In diesem Abschnitt schauen wir uns die Klasse /UI2/CL_JSON an, diese gibt es bereits in älteren Releases und stellt einen großen Umfang an Funktionen zur Verfügung.
JSON nach ABAP
In diesem Beispiel wollen wir die Daten von JSON in die ABAP Struktur konvertieren. Dazu verwenden wir die Methode DESERIALIZE der Klasse:
Dazu definieren wir im ersten Schritt unsere lokale Zielstruktur und erzeugen uns dann das JSON in Form eines Strings. Zum Abschluss rufen wir die Methode auf und lassen die Daten konvertieren. Über JSON können wir einen String übergeben (lesbar) und über JSONX einen XString (binär).
DATA ls_dummy TYPE ts_dummy_data.
DATA(ld_json) = get_dummy_json( ).
/ui2/cl_json=>deserialize( EXPORTING json = ld_json
CHANGING data = ls_dummy ).
Nachdem die Konvertierung durchgeführt wurde, schauen wir uns das Ergebnis im Debugger an. Die Daten wurden sauber gemacht und zugewiesen. Auch die Referenz ist befüllt und steht für die Verarbeitung bereit. Grundsätzlich musst du nicht alle Daten mappen. Wenn du Felder oder Strukturen weglässt, werden diese ignoriert und führen nicht zu einem Fehler.
ABAP nach JSON
In diesem Beispiel wollen wir nun unsere internen Daten nach JSON konvertieren und die verschiedenen Optionen der Klasse probieren. Im ersten Beispiel verwenden wir daher den einfachsten Fall und rufen die Methode SERIALIZE auf, ohne weitere Parameter zu befüllen.
DATA(ls_internal) = get_internal_data( ).
DATA(ld_json) = /ui2/cl_json=>serialize( data = ls_internal ).
Die Ausgabe ist entsprechend flach und praktisch, alles Felder werden in Großbuchstaben und mit Unterstrichen angelegt.
Im nächsten Beispiel nutzen wir den Parameter FORMAT_OUTPUT, damit können wir die Ausgabe lesbar formatieren, vor allem wenn wir die Daten in die Konsole oder eine Datei schreiben wollen.
ld_json = /ui2/cl_json=>serialize( data = ls_internal
format_output = abap_true ).
Die Ausgabe der Daten sieht nun wie folgt aus. Mit der Verwendung des richtigen Datentyps wird auch der boolsche Wert sauber konvertiert. Mit ABAP_BOOLEAN funktioniert aktuell die Konvertierung nicht.
Über den Parameter PRETTY_NAME können wir die Ausgabe der Felder steuern:
- /UI2/CL_JSON=>PRETTY_MODE-LOW_CASE - Anstatt komplett groß, werden die Feldnamen nun klein geschrieben.
- /UI2/CL_JSON=>PRETTY_MODE-CAMEL_CASE - Der erste Buchstabe bleibt klein, der erste Buchstabe nach einem Unterstrich wird groß und der Unterstrich entfernt.
Klasse XCO_CP_JSON
Die XCO Klassen sind noch recht neu in der ABAP Entwicklung und sind die neuen Schnittstellen für alle Arten von Objekten im Standard. Für die Konvertierung greifen wir daher auf die Klasse XCO_CP_JSON zu, welche für Zugriffe auf JSON konzipiert wurde.
JSON nach ABAP
Für die Konvertierung der Daten rufen wir zuerst einmal die Methode FROM_STRING auf, um den JSON String zu übergeben. Dann übergeben wir eine Transformation, damit der boolsche Wert nach ABAP-BOOL konvertiert wird. Über die Methode WRITE_TO schreiben wir die Daten in unsere lokale Struktur.
DATA ls_dummy TYPE ts_dummy_data.
DATA(ld_json) = get_dummy_json( ).
xco_cp_json=>data->from_string( ld_json )->apply( VALUE #( ( xco_cp_json=>transformation->boolean_to_abap_bool ) ) )->write_to(
REF #( ls_dummy ) ).
Hinweis: Aktuell funktioniert die Konvertierung nur, wenn wir die dynamische Struktur/Referenz aus dem Datentypen entfernen, ansonsten kommt es zum Laufzeitfehler XML_FORMAT_ERROR. Damit scheint die dynamische Konvertierung nicht zu funktionieren.
ABAP nach JSON
Wie können wir mit der neuen Klasse einen JSON String erzeugen? Dazu rufen wir über die Klasse das DATA Objekt auf und übergeben mit der Methode FROM_ABAP die Daten. Im Anschluss können wir mit der Methode TO_STRING einen JSON String generieren lassen.
DATA(lt_internal) = get_internal_data( ).
DATA(ld_json) = xco_cp_json=>data->from_abap( lt_internal )->to_string( ).
Wollen wir noch zusätzliche Formatter einsetzen, dann können wir das über die Methode APPLY machen. Dazu ein kleines Beispiel, wo wir einen Formatter hinzufügen:
ld_json = xco_cp_json=>data->from_abap( lt_internal )->apply(
VALUE #( ( xco_cp_json=>transformation->underscore_to_pascal_case ) )
)->to_string( ).
Folgende Formatierungsoptionen stehen uns zur Verfügung:
- XCO_CP_JSON=>TRANSFORMATION->UNDERSCORE_TO_PASCAL_CASE - Erster Buchstabe wird groß konvertiert, ebenso wie Buchstaben nach einem Unterstrich. Der Unterstrich wird immer entfernt.
- XCO_CP_JSON=>TRANSFORMATION->UNDERSCORE_TO_CAMEL_CASE - Erster Buchstabe bleibt klein, Buchstabennach einem Unterstrich werden groß konvertiert und der Unterstrich entfernt.
Hinweis: Eine Konvertierung von ABAP_BOOL auf einen boolschen Wert war bisher nicht möglich. Es werden die Werte von ABAP_TRUE und ABAP_FALSE als String in das Feld übernommen.
Dynamische Daten
Bei der Konvertierung von JSON nach ABAP, hatten wir auch eine dynamische Tabelle. Wie können wir diese nun sauber mappen und auf die Werte zugreifen? Im Grunde besteht die Referenz aus einer Struktur, die dynamisch viele Spalten hat, wobei der Spaltenname die Information zur Währung enthält und der Wert der Inhalt ist. Dazu benötigen wir erst die Information zu den Komponenten. Dafür weisen wir die Referenz einem Feldsymbol zu und holen uns über die Klasse CL_ABAP_TYPEDESCR eine Beschreibung der Struktur.
ASSIGN ls_dummy-dynamic_list->* TO FIELD-SYMBOL(<ls_dynamic>).
DATA(lo_description) = CAST cl_abap_structdescr( cl_abap_typedescr=>describe_by_data( <ls_dynamic> ) ).
Nun können wir über die einzelnen Komponenten der Struktur loopen und den Wert aus der Struktur einem Feldsymbol (<ld_value>) zuordnen. Mit dem Namen und
LOOP AT lo_description->components ASSIGNING FIELD-SYMBOL(<ls_componenent>).
ASSIGN COMPONENT <ls_componenent>-name OF STRUCTURE <ls_dynamic> TO FIELD-SYMBOL(<ld_value>).
ENDLOOP.
Nach der Konvertierung des Wertes in unseren Zieldatentyp, geben wir das Ergebnis in die Konsole aus. Damit können wir auch die dynamischen Typen mappen. Grundsätzlich empfehlen wir dir aber, den Typen direkt auf ABAP Typen zu mappen, wenn es geht, da du dir dadurch Arbeit und Zeit sparst.
Komplettes Beispiel
Hier findest du noch einmal die vollständige Klasse mit allen gezeigten Beispielen und der entsprechenden Ausgabe. Du kannst die Klasse auch zum Debuggen der Ausgaben und Informationen nutzen.
CLASS zcl_bs_demo_json DEFINITION
PUBLIC FINAL
CREATE PUBLIC.
PUBLIC SECTION.
INTERFACES if_oo_adt_classrun.
PRIVATE SECTION.
TYPES td_decimal TYPE p LENGTH 16 DECIMALS 2.
TYPES td_char TYPE c LENGTH 10.
TYPES: BEGIN OF ts_element,
text2 TYPE string,
number2 TYPE i,
END OF ts_element.
TYPES tt_element TYPE STANDARD TABLE OF ts_element WITH EMPTY KEY.
TYPES tt_data TYPE STANDARD TABLE OF td_char WITH EMPTY KEY.
TYPES: BEGIN OF ts_dummy_data,
text TYPE string,
number_integer TYPE i,
number_decimal TYPE td_decimal,
boolean TYPE abap_bool,
array_element TYPE tt_element,
array_data TYPE tt_data,
dynamic_list TYPE REF TO data,
END OF ts_dummy_data.
TYPES: BEGIN OF ts_internal,
camel_string TYPE string,
singlestring TYPE string,
camel_number TYPE td_decimal,
singlenumber TYPE td_decimal,
camel_int TYPE i,
singleint TYPE i,
camel_bool TYPE abap_bool,
singlebool TYPE abap_bool,
END OF ts_internal.
TYPES tt_internal TYPE STANDARD TABLE OF ts_internal WITH EMPTY KEY.
METHODS get_dummy_json
RETURNING VALUE(rd_result) TYPE string.
METHODS get_internal_data
RETURNING VALUE(rt_result) TYPE tt_internal.
METHODS convert_json_with_ui2
IMPORTING io_out TYPE REF TO if_oo_adt_classrun_out.
METHODS convert_json_with_xco
IMPORTING io_out TYPE REF TO if_oo_adt_classrun_out.
METHODS convert_int_to_json_with_ui2
IMPORTING io_out TYPE REF TO if_oo_adt_classrun_out.
METHODS convert_dynamic_data
IMPORTING io_out TYPE REF TO if_oo_adt_classrun_out.
METHODS convert_int_to_json_with_xco
IMPORTING io_out TYPE REF TO if_oo_adt_classrun_out.
ENDCLASS.
CLASS zcl_bs_demo_json IMPLEMENTATION.
METHOD if_oo_adt_classrun~main.
convert_json_with_ui2( out ).
convert_json_with_xco( out ).
convert_int_to_json_with_ui2( out ).
convert_int_to_json_with_xco( out ).
convert_dynamic_data( out ).
ENDMETHOD.
METHOD get_dummy_json.
rd_result = |\{| &&
| "text": "My text",| &&
| "number_integer": 37,| &&
| "number_decimal": 10.12,| &&
| "boolean": true,| &&
| "array_element": [| &&
| \{| &&
| "text2": "A",| &&
| "number2": 1| &&
| \},| &&
| \{| &&
| "text2": "B",| &&
| "number2": 2| &&
| \},| &&
| \{| &&
| "text2": "C",| &&
| "number2": 3| &&
| \}| &&
| ],| &&
| "array_data": [| &&
| "A-A",| &&
| "A-B",| &&
| "B-A"| &&
| ],| &&
| "dynamic_list": {| &&
| "AED": 12.50,| &&
| "EUR": 5.20,| &&
| "USD": 9.96| &&
| \}| &&
|\}|.
ENDMETHOD.
METHOD get_internal_data.
rt_result = VALUE #( ( camel_string = 'String 1'
singlestring = 'String 2'
camel_number = '9.95'
singlenumber = '5.59'
camel_int = 2
singleint = 12
camel_bool = abap_true
singlebool = abap_false ) ).
ENDMETHOD.
METHOD convert_json_with_ui2.
DATA ls_dummy TYPE ts_dummy_data.
DATA(ld_json) = get_dummy_json( ).
/ui2/cl_json=>deserialize( EXPORTING json = ld_json
CHANGING data = ls_dummy ).
ENDMETHOD.
METHOD convert_json_with_xco.
DATA ls_dummy TYPE ts_dummy_data.
DATA(ld_json) = get_dummy_json( ).
xco_cp_json=>data->from_string( ld_json )->apply( VALUE #( ( xco_cp_json=>transformation->boolean_to_abap_bool ) ) )->write_to(
REF #( ls_dummy ) ).
ENDMETHOD.
METHOD convert_int_to_json_with_ui2.
DATA(ls_internal) = get_internal_data( ).
DATA(ld_json) = /ui2/cl_json=>serialize( data = ls_internal ).
io_out->write( `Build raw:` ).
io_out->write( ld_json ).
ld_json = /ui2/cl_json=>serialize( data = ls_internal
format_output = abap_true ).
io_out->write( `Format output:` ).
io_out->write( ld_json ).
ld_json = /ui2/cl_json=>serialize( data = ls_internal
pretty_name = /ui2/cl_json=>pretty_mode-low_case
format_output = abap_true ).
io_out->write( `Pretty low-case:` ).
io_out->write( ld_json ).
ld_json = /ui2/cl_json=>serialize( data = ls_internal
pretty_name = /ui2/cl_json=>pretty_mode-camel_case
format_output = abap_true ).
io_out->write( `Pretty camel-case:` ).
io_out->write( ld_json ).
ld_json = /ui2/cl_json=>serialize( data = ls_internal
conversion_exits = abap_true ).
io_out->write( `Test:` ).
io_out->write( ld_json ).
ENDMETHOD.
METHOD convert_dynamic_data.
DATA ls_dummy TYPE ts_dummy_data.
DATA(ld_json) = get_dummy_json( ).
/ui2/cl_json=>deserialize( EXPORTING json = ld_json
CHANGING data = ls_dummy ).
ASSIGN ls_dummy-dynamic_list->* TO FIELD-SYMBOL(<ls_dynamic>).
DATA(lo_description) = CAST cl_abap_structdescr( cl_abap_typedescr=>describe_by_data( <ls_dynamic> ) ).
LOOP AT lo_description->components ASSIGNING FIELD-SYMBOL(<ls_componenent>).
ASSIGN COMPONENT <ls_componenent>-name OF STRUCTURE <ls_dynamic> TO FIELD-SYMBOL(<ld_value>).
io_out->write( |Currency: { <ls_componenent>-name } with value: { CONV td_decimal( <ld_value>->* ) }| ).
ENDLOOP.
ENDMETHOD.
METHOD convert_int_to_json_with_xco.
DATA(lt_internal) = get_internal_data( ).
DATA(ld_json) = xco_cp_json=>data->from_abap( lt_internal )->to_string( ).
io_out->write( `Raw output:` ).
io_out->write( ld_json ).
ld_json = xco_cp_json=>data->from_abap( lt_internal )->apply(
VALUE #( ( xco_cp_json=>transformation->underscore_to_pascal_case ) )
)->to_string( ).
io_out->write( `Apply underscore_to_pascal_case:` ).
io_out->write( ld_json ).
ld_json = xco_cp_json=>data->from_abap( lt_internal )->apply(
VALUE #( ( xco_cp_json=>transformation->underscore_to_camel_case ) )
)->to_string( ).
io_out->write( `Apply underscore_to_camel_case:` ).
io_out->write( ld_json ).
ENDMETHOD.
ENDCLASS.
ABAP Cloud
Für ABAP Cloud gibt es aktuell zwei unterschiedliche freigegebene Klassen, einmal den Klassiker /UI2/CL_JSON und die neuen XCO Library XCO_CP_JSON. Hier findest du noch einmal die Zusammenfassung der beiden Klassen und den Vergleich der Features:
Feature | /UI2/CL_JSON | XCO_CP_JSON |
---|---|---|
ABAP_BOOL -> BOOL | ✔️ | |
BOOL -> ABAP_BOOL | ✔️ | ✔️ |
Formatter UPPER_FORMAT | ✔️ | ✔️ |
Formatter lower_format | ✔️ | |
Formatter camelCase | ✔️ | ✔️ |
Formatter PascalCase | ✔️ | |
Dynamische Daten | ✔️ | |
Output Formatter | ✔️ | |
Zusätzliches Feldmapping | ✔️ |
Bis auf einen fehlenden Formatter empfehlen wir so weit immer noch die Nutzung der Klasse /UI2/CL_JSON, wenn sie deinen Anforderungen entspricht.
Verfügbarkeit
Aktuell ist die Klasse /UI2/CL_JSON nur im ABAP Environment und der Public Cloud verfügbar mit C1-Contract. Es gibt aber bereits eine Zusicherung für S/4HANA 2023 FPS2, dass die Klasse geliefert wird. Aktuell bleiben dir daher zwei Möglichkeiten zur Nutzung:
- Erstellung eines Wrappers
- Setzen des C1-Contracts am Objekt (Modifikation)
Aktuell sind wir im Austausch, ob eine Vorablieferung per Hinweis möglich ist.
Fazit
In diesem Artikel wollten wir dir die Arbeit mit JSON in ABAP etwas näherbringen. Du solltest nun den Aufbau der Struktur einfacher verstehen und kannst diese in ABAP abbilden. Mit den beiden Klassen kannst du die Konvertierung auf interne Strukturen durchführen und so HTTP Schnittstellen implementieren und deren Daten verarbeiten.
Weitere Informationen:
GitHub - ABAP to JSON (unter Dokumentation)
SAP BTP - JSON