Skip to Content
Technical Articles
Author's profile photo Alexander Tsybulsky

Bicycles. #2 – AJSON – yet another abap json parser and serializer

Preamble and disclaimer

Creating own implementation for some problem, which was already solved is before, often referred as “inventing a bicycle”. I did invent a couple too 🙂 This post, and maybe a couple of following posts,  describe several of my open-source “bicycles”. I “invented” them because in the time I was searching for such functionalities that I didn’t find them. I’m suspect these functionalities do exist already and you can point that out in comments, if you know a better and more standard solution. Yet my “bicycles” were “invented” with the convenience in mind and with an attempt to make clean and universal tools. So I sincerely hope someone will find them useful. If I’m missing an obvious solution – welcome to comment 🙂 KR. Alexander.

AJSON

Once I read a comment by Paul Hardy (if I remember well) to a blog post that “Every week someone publishes a post about own excel or json parser”. I don’t remember the exact wording, but think the essence is clear. So I proudly present one more proof to that statement 🙂

Why re-invent an abap json parser? There are plenty of other parsers already, but I didn’t find exactly what I wanted. Let’s list some of widely known approaches.
The above libraries are cool. Nothing wrong with them. But they share one approach – take json string, convert it into an abap structure/table and vice versa (except UI5 one). While working with web APIs I faced a couple of specifics which are not easily addressed with this approach.
  • separating API response parts – properly designed REST APIs often wrap the payload data into a typical API header e.g. `{ success: 1, error: “xyz”, payload: … }`. I wanted to separate processing of API response layer from the payload processing. So to “extract” payload as a root node and clean it from header stuff for the further processing
  • flexibity – APIs may change, especially if the other side is an evolving custom service. Changing the abap structures to match is a pain
  • partial data extraction – what if I don’t need the whole json data structure, but only “one array there”, why should I reflect the whole API structure in abap?
So I wanted a json parser that would be:
  • 2-directional
  • easy to install (no OSS notes)
  • compatible with 702/731
  • convenient to use in terms of code (oh, that’s important!)
  • able to convert to/from abap
  • able to access parts of json specifically without complete structure conversion
After trying some alternatives I created my own implementation to match my need and coding convenience I wanted. Let alone the fact that it was a fun (which is quite important!). The class can read json, access it’s parts flexibly, slice (extract) parts, make the json immutable (if required), set json attributes/arrays, convert the data to and from abap structures and serialize all that back to json with or without indentation. If I’m not lazy (or if there will be a practical need) I might also implement json schema validation.

Bellow are some examples which should be self explainable.

The code is open-sourced, heavily covered with unit tests and published under MIT license here: https://github.com/sbcgua/ajson. Can be installed using abapGit.

Functionality and Examples

Basics

  • To parse existing json data – call `zcl_ajson=>parse( lv_json_string )`
  • To create a new empty json instance (to set values and serialize) – call `zcl_ajson=>create_empty( )`
  • Json attributes are addressed by path in form `/obj1/obj2/value` of e.g. `/a/b/c` addresses `{ a: { b: { c: “this value !” } } }`
  • Array items addressed with index starting from 1: `/tab/2/val` -> `{ tab: [ {…}, { val: “this value !” } ] }`

JSON reader

The reading is done via `zif_ajson_reader` interface, or aliases of the main class. The methods of interface allows accessing attributes and converting to abap structure.
Examples below assume original json was:
{
  "success": 1,
  "error": "",
  "payload": {
    "text": "hello",
    "num": 123,
    "bool": true,
    "false": false,
    "null": null,
    "date": "2020-07-28",
    "table": [
      "abc",
      "def"
    ]
  }
}

Individual values

data r type ref to zif_ajson_reader.
r = zcl_ajson=>parse( lv_json_string_from_above ).

r->exists( '/success' ).            " returns abap_true

r->get( '/success' ).               " returns "1"
r->get_integer( '/success' ).       " returns 1 (number)
r->get_boolean( '/success' ).       " returns "X" (abap_true - because not empty)

r->get( '/payload/bool' ).          " returns "true"
r->get_boolean( '/payload/bool' ).  " returns "X" (abap_true)

r->get( '/payload/false' ).         " returns "false"
r->get_boolean( '/payload/false' ). " returns "" (abap_false)

r->get( '/payload/null' ).          " returns "null"
r->get_string( '/payload/null' ).   " returns "" (empty string)

r->get( '/payload/date' ).          " returns "2020-07-28"
r->get_date( '/payload/date' ).     " returns "20200728" (type d)

r->members( '/' ).                  " returns string table of "success", "error", "payload"

Segment slicing

" Slice returns zif_ajson_reader instance but "payload" becomes root
" Useful to process API responses with unified wrappers
data payload type ref to zif_ajson_reader.
payload = r->slice( '/payload' ). 
payload->get( '/text' ). " Now the root has changed

Converting to abap structure

data:
  begin of ls_payload,
    text type string,
    num type i,
    bool type abap_bool,
    false type abap_bool,
    null type string,
    table type string_table, " Array !
  end of ls_payload.

payload->to_abap( importing ev_container = ls_payload ).

JSON writer

Modification of JSON is accessible via `zif_ajson_writer` interface of directly via aliases in the main class. The methods of interface allows setting attributes, objects, arrays.

Individual values

data w type ref to zif_ajson_writer.
w = zcl_ajson=>create_empty( ).

" Set value
" Results in { a: { b: { num: 123, str: "hello", bool: true } } }
" The intermediary path is auto created, value type auto detected
w->set(
  iv_path = '/a/b/num'
  iv_val  = 123 ).
w->set(
  iv_path = '/a/b/str'
  iv_val  = 'hello' ).
w->set(
  iv_path = '/a/b/bool'
  iv_val  = abap_true ).
w->set(
  iv_path = '/a/b/str'
  iv_val  = 'escaping"\' ). " => "escaping\"\\", also with \n, \r, \t

" Ignoring empty values by default
w->set(
  iv_path = '/a'
  iv_val  = abap_false ). " => nothing added to json !!!
w->set(
  iv_ignore_empty = abap_false
  iv_path = '/a'
  iv_val  = abap_false ). " => "a": false
w->set(
  iv_path = '/a'
  iv_val  = 0 ). " => nothing added to json !!!
w->set(
  iv_ignore_empty = abap_false
  iv_path = '/a'
  iv_val  = 0 ). " => "a": 0

Individual TYPED values

" Set typed value
" IMPORTANTLY, empty values are always not ignored !
" Booleans -> converts not initial values to true
w->set_boolean(
  iv_path = '/a'
  iv_val  = 123 ). " => true
w->set_boolean( " empty value not ignored !
  iv_path = '/a'
  iv_val  = 0 ). " => false
w->set_boolean(
  iv_path = '/a'
  iv_val  = 'abc' ). " => true
w->set_boolean(
  iv_path = '/a'
  iv_val  = lt_non_empty_tab ). " => true

" Integer
w->set_integer( " this just forces conversion to int at param level
  iv_path = '/a'
  iv_val  = 123 ). " => 123
w->set_integer( " empty value not ignored !
  iv_path = '/a'
  iv_val  = 0 ). " => 0

" String (clike param)
w->set_string(
  iv_path = '/a'
  iv_val  = sy-datum ). " => e.g. 20200705
w->set_string( " empty value not ignored !
  iv_path = '/a'
  iv_val  = '' ). " => "a": ""

" Date - converts date param to json formatted date
w->set_string(
  iv_path = '/a'
  iv_val  = sy-datum ). " => e.g. "2020-07-05" (with dashes)

" Null
" same effect is for initial data ref
w->set_null(
  iv_path = '/a' ). " => "a": null

Deletion and rewriting

" Importantly, values and whole branches are rewritten
" { a: { b: 0 } } - the old "b" completely deleted
w->set(
  iv_path = '/a/b'
  iv_val  = 0 ).

" Items can be deleted explicitly
w->delete( '/a/b' ). " => { a: { } }

" Or completely cleared
w->clear( ).

Settings objects

`set` method is smart – it detects the type of input value automatically.

 

" Set object
" Results in { a: { b: { payload: { text: ..., num: ... } } } }
data:
  begin of ls_payload,
    text type string,
    num type i,
  end of ls_payload.
w->set(
  iv_path = '/a/b/payload'
  iv_val  = ls_payload ).

" Set other object with ajson instance
w->set(
  iv_path = '/a/b/payload'
  iv_val  = lo_another_ajson ).

Settings arrays/tables


" Set arrays
" Results in: { array: [ "abc", "efg" ] }
" Tables of structures, of tables, and other deep objects are supported as well
data tab type string_table.
append 'abc' to tab.
append 'efg' to tab.
w->set(
  iv_path = '/array'
  iv_val  = tab ).

" Fill arrays item by item
" Different types ? no problem
w->push(
  iv_path = '/array'
  iv_val  = 1 ).
" => { array: [ "abc", "efg", 1 ] }

w->push(
  iv_path = '/array'
  iv_val  = ls_payload ).
" => { array: [ "abc", "efg", 1, { text: ..., num: ... } ] }

" Push verifies that the path item exists and is array
" it does NOT auto create path like "set"
" to explicitly create an empty array use "touch_array"
w->touch_array( '/array2' ).
" => { array: ..., array2: [] }

 

Freezing json (read only)

It is possible to set an instance of ajson immutable (read only). It is done on object level with method `freeze` or at parse time with `iv_freeze = abap_true` param. This is one way only change. After this `set`, `delete`, `clear` and other modification methods will raise exceptions if used. Useful to freeze some kind of settings or service responses to avoid subtle bugs.

Rendering to a JSON string

`zcl_ajson` instance content can be rendered to JSON string using `stringify` method. It also supports optional indentation.

    data lo_json type ref to zcl_ajson.
    data li_writer type ref to zif_ajson_writer.

    lo_json   = zcl_ajson=>create_empty( ).
    li_writer = lo_json.

    li_writer->set(
      iv_path = '/a'
      iv_val  = 1 ).
    li_writer->set(
      iv_path = '/b'
      iv_val  = 'B' ).
    li_writer->touch_array(
      iv_path = '/e' ).
    li_writer->touch_array(
      iv_path = '/f' ).
    li_writer->push(
      iv_path = '/f'
      iv_val  = 5 ).

    data lv type string.
    lv = lo_json->stringify( ).
    " {"a":1,"b":"B","e":[],"f":[5]}

    lv = lo_json->stringify( iv_indent = 2 ). " indent with 2 spaces
    " {
    "   "a": 1,
    "   "b": "B",
    "   "e": [],
    "   "f": [
    "     5
    "   ]
    " }
For more examples see unit tests code

P.S. …one more use case

One more use case I’d like to share. My team and I are developing custom SAP related products and at the beginning of development it is not visible what kind of customizing will be required and the shape of it. And if you need to change the shape of settings after the code is written (due to a request from a customer or due to mere evolution of the product) you have to change tables, maintenance views, and all this stuff which is a headache. Recently, we’ve been experimenting with approach used by abapGit – a table with a string field with freely definable XML (JSON in our case). This enable flexibility which is quite handy for developing of quickly evolving code. The solution maybe arguable, but it looks very convenient so far. I might write a separate blog post on the subject and maybe even publish some code, if there is interest.

 

I hope you find this useful or at least interesting. 🙂

Assigned Tags

      2 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Shai Sinai
      Shai Sinai

      Thanks for sharing.

      One short question (since you've mentioned you want this class to be convenient to use):

      Why do you need all these typed get/set methods (set_string, set_integer, etc.) instead of using one method which implicitly checks the datatype of the sent parameter?

      Author's profile photo Alexander Tsybulsky
      Alexander Tsybulsky
      Blog Post Author

      It does. However, if you for whatever reason what to override the type - you can. E.g. supply an integer but render it as a string, or e.g. boolean - you may want it as X or as true. I guess it is more relevant to getters though.