GithubHelp home page GithubHelp logo

ajson's Introduction

abaplint abap package version

abap json (ajson)

Yet another json parser/serializer for ABAP. It works with release 7.02 or higher.

BREAKING CHANGES in v1.1

  • zif_ajson_reader and zif_ajson_writer interface removed. Use zif_ajson. The last version with those interfaces is v1.0.4.

DEPRECATION NOTES

  • since v1.1.7
    • there are changes in mapper interface, see Mapping (field renaming) section below. In essence, implement rename_node method if needed, to_json and to_abap will be deprecated. As well as create_field_mapping and create_camel_case mappers
    • potentially create_empty static method may be deprecated. It is considered to use new instead (and/or direct creation create object). Under consideration, post an issue if you have an opinion on this subject.
    • also create_from is potentially suboptimal, so prefer clone, filter and map instead.

Features

  • parse into a flexible form, not fixed to any predefined data structure, allowing to modify the parsed data, selectively access its parts and slice subsections of it
    • slicing can be particularly useful for REST header separation e.g. { "success": 1, "error": "", "payload": {...} } where 1st level attrs are processed in one layer of your application and payload in another (and can differ from request to request)
  • allows conversion to fixed abap structures/tables (to_abap)
  • convenient interface to manipulate the data - set( value ), set( structure ), set( table ), set( another_instance_of_ajson ), also typed e.g. set_date
    • also setx for text-based value setting like setx( '/a/b:123' ) (useful e.g. for constants in APIs or in unit-tests)
  • seralization to string
  • freezing (read only) instance content
  • filtering. Create a json skipping empty values, predefined paths, or your custom filter. EXPERIMENTAL, interface may change
  • utility to calculate difference between 2 jsons

Installed using abapGit

Examples and documentation

Instantiating and 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( )
  • All functional methods and types are defined via zif_ajson interface. Methods have alias in the zcl_ajson class, however please restrain from using them directly as they may be deprecated in future.
  • 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 !" } ] }
  • Mapping and formatting options are available with interface zif_ajson_mapping. Predefined types for field mapping (ABAP <=> JSON), Camel Case, UPPER/lower case from class zcl_ajson_mapping

JSON reading

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 value reading

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

r->is_empty( ).                     " returns abap_false
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 table of "success", "error", "payload"

Segment slicing

" Slice returns zif_ajson instance but "payload" becomes root
" Useful to process API responses with unified wrappers
data payload type ref to zif_ajson.
payload = r->slice( '/payload' ). 

Getting node type

In some case you might want to know node type prior to accessing it first. Type can be 'str', 'num', 'null', 'bool', 'object', 'array'.

r->get_node_type( '/payload/false' ).         " returns "bool"
r->get_node_type( '/payload/text' ).          " returns "str"

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 ).

to_abap supports transferring "corresponding only" fields.

payload->to_abap( 
  exporting iv_corresponding = abap_true
  importing ev_container     = ls_payload ).

" Or via an instance flag (persists after setting!)
payload->to_abap_corresponding_only( )->to_abap( importing ev_container = ls_payload ).

JSON writing

The methods of interface allows setting attributes, objects, arrays.

Individual value writing

data w type ref to zif_ajson.
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

" With explicit type
w->set(
  iv_path      = '/a'
  iv_val       = '0'
  iv_node_type = 'num' ). " => "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_date(
  iv_path = '/a'
  iv_val  = sy-datum ). " => e.g. "2020-07-05" (with dashes)

" Timestamp - converts timestamp param to json formatted data as ISO (<YYYY>-<MM>-<DD>T<HH>:<MM>:<SS>Z)
get time stamp field lv_timestamp.
w->set_timestamp(
  iv_path = '/a'
  iv_val  = lv_timestamp ). " => e.g. "2021-05-05T12-00-00Z" (with dashes)

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

Text-based set

The method setx is a shortcut for full-scale set, it attempts to parse a string and detect both path and value from it. Although it is less performant (!) but it is more readable which can be beneficial for some cases where it is not critical e.g. setting constants in APIs or unit tests.
Format: path and value are separated by ':', space around path and around value is trimmed.

j->setx( '/a: 1' ).     " { "a": 1 }
j->setx( '/a: 1.123' ). " { "a": 1.123 }
j->setx( '/a: abc' ).   " { "a": "abc" }
j->setx( '/a: "abc"' ). " { "a": "abc" }
j->setx( '/a: "123"' ). " { "a": "123" } - force string
j->setx( '/a: null' ).  " { "a": null }
j->setx( '/a: true' ).  " { "a": true }
j->setx( '/a: false' ). " { "a": false }

" deep path are supported
j->setx( '/a/b/c: 1' ).

" and also arrays and objects
" Note, the object must be in complete json format, with ""
j->setx( '/a: { "b": "abc" }' ). 
j->setx( '/a: [1,2,3]' ). 

" The method is chainable
j->setx( '/a: 1' )->setx( '/b: 2' ).

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 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": [] }

Setting data refs

Currently not supported, but maybe in future. Except initial data ref which is equivalent to set_null.

Chaining

Set (and some other) methods also return me to support chaining: li_json->set(...)->set(...)->touch_array(...)->push(...).

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.

Rendering to JSON string

zcl_ajson instance content can be rendered to JSON string using zif_ajson~stringify method (also has alias at class level). It also supports optional indentation.

    data lo_json type ref to zcl_ajson.
    data li_json type ref to zif_ajson.

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

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

    data lv type string.
    lv = lo_json->stringify( ). " or li_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
    "   ]
    " }

Keep item order

Sometimes you may want to keep order of json items in the same order as it was in abap structure (assuming you set structures or table of structures). To do this: set iv_keep_item_order flag when creating an instance or call keep_item_order after creation of instance, before any set.

  data:
    begin of ls_dummy,
      zulu type string,
      alpha type string,
      beta type string,
    end of ls_dummy.

  li_json->keep_item_order( ).
  li_json->set(
    iv_path = '/'
    iv_val  = ls_dummy ).
  li_json->stringify( ). " '{"zulu":"z","alpha":"a","beta":"b"}'
  " otherwise - '{"alpha":"a","beta":"b","zulu":"z"}'

  " OR
  li_json = zcl_ajson=>new( iv_keep_item_order = abap_true ).
  ...

The same parameter exists for parsing

  li_json = zcl_ajson=>parse( 
    iv_json            = '{"b":1,"a":2}'
    iv_keep_item_order = abap_true ).
  li_json->stringify( ). " '{"b":1,"a":2}'

Auto format date/time

By default date, time and timestamp dates are not formatted and are written in abap format as 'YYYYMMDD', 'HHMMSS'. This can be changed by calling format_datetime method after creation. After that the date/time will be auto-formatted as 'YYYY-MM-DD' and 'HH:MM:SS' respectively. Important: this may become the default behavior in future version

  data:
    begin of ls_dummy,
      date type d value '20220412',
    end of ls_dummy.

  li_json->format_datetime( ).
  li_json->set(
    iv_path = '/'
    iv_val  = ls_dummy ).
  li_json->stringify( ). " {"date":"2022-04-12"}'
  " otherwise - {"date":"20220412"}

Timestamps

Conversion from JSON to ABAP can determine automatically if the value is a timestamp if:

  • value has timestamp format YYYY-MM-DDThh:mm:ssTZD, where
    • YYYY = four-digit year
    • MM = two-digit month (01=January, etc.)
    • DD = two-digit day of month (01 through 31)
    • hh = two digits of hour (00 through 23) (am/pm NOT allowed)
    • mm = two digits of minute (00 through 59)
    • ss = two digits of second (00 through 59)
    • TZD = time zone designator (Z or +hh:mm or -hh:mm)
  • abap base type of field is P (Packed)

Examples

Using a json with possible formats:

{
  "date":"2020-07-28",
  "datetime":"2020-07-28T00:00:00",
  "datetime_utc":"2020-07-28T00:00:00Z",
  "datetime_plus1":"2020-07-28T01:00:00+01:00"
}

Can be mapped to following structure:

  DATA:
    BEGIN OF json_timestamp,
      date           TYPE d,
      datetime       TYPE timestamp,
      datetime_utc   TYPE timestamp,
      datetime_plus1 TYPE timestamp,
    END OF json_timestamp.

  DATA(lo_ajson) = zcl_ajson=>parse( json_content ).

  lo_ajson->to_abap( IMPORTING ev_container = json_timestamp ).

Cloning

  lo_new_json = lo_orig_json->clone( ). " results in new independent json copy
  " OR ... (but prefer the former)
  lo_new_json = zcl_ajson=>create_from(
    ii_source_json = lo_orig_json ).

Mapping (field renaming)

You can rename json attribute (node) names with a mapper. Typical example for this is making all attribute names upper/lower case or converting camel-snake naming styles (e.g. helloWorld -> hello_world).

  lo_orig_json = zcl_ajson=>parse( '{"ab":1,"bc":2}' ).
  lo_new_json = lo_orig_json->map( li_mapper ). -> " E.g. '{"AB":1,"BC":2}'
  " OR ... (but prefer the former)
  lo_new_json = zcl_ajson=>create_from(
    ii_source_json = lo_orig_json
    ii_mapper      = li_mapper ). 

where li_mapper would be an instance of zif_ajson_mapping.

AJSON implements a couple of frequent convertors in zcl_ajson_mapping class, in particular:

  • upper/lower case
  • to camel case (camelCase)
  • to snake case (snake_case)

You can also implement you custom mapper. To do this you have to implement zif_ajson_mapping->rename_node(). It accepts the json nodes item-by-item and may change name via cv_name parameter. E.g.

  method zif_ajson_mapping~rename_field.
    if cv_name+0(1) = 'a'. " Upper case all fields that start with "a"
      cv_name = to_upper( cv_name ).
    endif.
  endmethod.

A realistic use case would be converting an external API result, which are often camel-cased (as this is very common in java script world), and then converting it into abap structure:

  data:
    begin of ls_api_response,
      error_code type string,
      ...
    end of ls_api_response.

  lo_orig_json = zcl_ajson=>parse( lv_api_response_string ). " { "errorCode": 0, ... }
  lo_new_json = zcl_ajson=>create_from(
    ii_source_json = lo_orig_json
    ii_mapper      = zcl_ajson_mapping=>camel_to_snake( ) ).
  lo_new_json->to_abap( importing ev_container = ls_api_response )

... or simpler and chained (combined with filter) ...

  zcl_ajson=>parse( lv_api_response_string
    )->filter( zcl_ajson_filter_lib=>create_path_filter(
      iv_skip_paths = '*/@*' " remove meta attributes
      iv_pattern_search = abap_true ) )
    )->map( zcl_ajson_mapping=>camel_to_snake( )
    )->to_abap( importing ev_container = ls_api_response ).

"Boxed-in" mappers

Several typical mappers were implemented within zcl_ajson_mapping class:

  • upper case node names
zcl_ajson=>parse( '{"a":1,"b":{"c":2}}'
  )->map( zcl_ajson_mapping=>create_upper_case( ) ).
  " {"A":1,"B":{"C":2}}
  • lower case node names
zcl_ajson=>parse( '{"A":1,"B":{"C":2}}'
  )->map( zcl_ajson_mapping=>create_lower_case( ) ).
  " {"a":1,"b":{"c":2}}
  • rename nodes
" Purely by name
zcl_ajson=>parse( '{"a":1,"b":{"c":2},"d":{"e":3}}'
  )->map( zcl_ajson_mapping=>create_rename( value #(
    ( from = 'a' to = 'x' )
    ( from = 'c' to = 'y' )
    ( from = 'd' to = 'z' ) )
  ) ).
  " {"b":{"y":2},"x":1,"z":{"e":3}}

" Or by full path
zcl_ajson=>parse( '{"a":1,"b":{"a":2},"c":{"a":3}}'
  )->map( zcl_ajson_mapping=>create_rename(
    it_rename_map = value #( ( from = '/b/a' to = 'x' ) )
    iv_rename_by  = zcl_ajson_mapping=>rename_by-full_path
  ) ).
  " {"a":1,"b":{"x":2},"c":{"a":3}}

" Or by pattern
zcl_ajson=>parse( '{"andthisnot":1,"b":{"thisone":2},"c":{"a":3}}'
  )->map( zcl_ajson_mapping=>create_rename(
    it_rename_map = value #( ( from = '/*/this*' to = 'x' ) )
    iv_rename_by  = zcl_ajson_mapping=>rename_by-pattern
  ) ).
  " {"andthisnot":1,"b":{"x":2},"c":{"a":3}}
  • combine several arbitrary mappers together
zcl_ajson=>parse( '{"a":1,"b":{"a":2},"c":{"a":3}}'
  )->map( zcl_ajson_mapping=>create_compound_mapper(
    ii_mapper1 = zcl_ajson_mapping=>create_rename(
      it_rename_map = value #( ( from = '/b/a' to = 'x' ) )
      iv_rename_by  = zcl_ajson_mapping=>rename_by-full_path )
    ii_mapper2 = zcl_ajson_mapping=>create_upper_case( ) )
  ).
  " {"A":1,"B":{"X":2},"C":{"A":3}}'
  • convert node names to snake case
zcl_ajson=>parse( '{"aB":1,"BbC":2,"cD":{"xY":3},"ZZ":4}'
  )->map( zcl_ajson_mapping=>create_to_snake_case( ) ).
  " {"a_b":1,"bb_c":2,"c_d":{"x_y":3},"zz":4}
  • convert node names to camel case
zcl_ajson=>parse( '{"a_b":1,"bb_c":2,"c_d":{"x_y":3},"zz":4}'
  )->map( zcl_ajson_mapping=>create_to_camel_case( ) ).
  " {"aB":1,"bbC":2,"cD":{"xY":3},"zz":4}

" Optionally upper case first letter too
zcl_ajson=>parse( '{"aj_bc":1}'
  )->map( zcl_ajson_mapping=>create_to_camel_case(
    iv_first_json_upper = abap_true ) ).
  " {"AjBc":1}

All the above examples will also work with static create_from() method (but don't prefer it, might be deprecated).

zcl_ajson=>create_from(
  ii_source_json = zcl_ajson=>parse( '{"aj_bc":1}' )
  ii_mapper = zcl_ajson_mapping=>create_to_camel_case( )
).
  " {"ajBc":1}

Mapping via to_abap and to_json (DEPRECATED)

This approach is depreciated and will be removed in future versions, please use rename_field approach described above

The interface zif_ajson_mapping allows to create custom mapping for ABAP and JSON fields via implementing to_abap and to_json methods.

Some mappings are provided by default:

  • ABAP <=> JSON mapping fields
  • JSON formatting to Camel Case
  • JSON formatting to UPPER/lower case

Example: JSON => ABAP mapping fields

JSON Input

{"field":"value","json.field":"field_value"}

Example code snippet

  data:
    lo_ajson          type ref to zcl_ajson,
    li_mapping        type ref to zif_ajson_mapping,
    lt_mapping_fields type zif_ajson_mapping=>ty_mapping_fields,
    ls_mapping_field  like line of lt_mapping_fields.
  data:
    begin of ls_result,
      abap_field type string,
      field      type string,
    end of ls_result.

  clear ls_mapping_field.
  ls_mapping_field-abap  = 'ABAP_FIELD'.
  ls_mapping_field-json = 'json.field'.
  insert ls_mapping_field into table lt_mapping_fields.

  li_mapping = zcl_ajson_mapping=>create_field_mapping( lt_mapping_fields ).

  lo_ajson =
      zcl_ajson=>parse( iv_json = '{"field":"value","json.field":"field_value"}' ii_custom_mapping = li_mapping ).

  lo_ajson->to_abap( importing ev_container = ls_result ).

Example: ABAP => JSON mapping fields

Example code snippet

  data:
    lo_ajson          type ref to zcl_ajson,
    li_mapping        type ref to zif_ajson_mapping,
    lt_mapping_fields type zif_ajson_mapping=>ty_mapping_fields,
    ls_mapping_field  like line of lt_mapping_fields.
  data:
    begin of ls_result,
      abap_field type string,
      field      type string,
    end of ls_result.

  clear ls_mapping_field.
  ls_mapping_field-abap  = 'ABAP_FIELD'.
  ls_mapping_field-json = 'json.field'.
  insert ls_mapping_field into table lt_mapping_fields.

  li_mapping = zcl_ajson_mapping=>create_field_mapping( lt_mapping_fields ).

  ls_result-abap_field = 'field_value'.
  ls_result-field      = 'value'.

  lo_ajson = zcl_ajson=>create_empty( ii_custom_mapping = li_mapping ).

  lo_ajson->set( iv_path = '/' iv_val = ls_result ).

JSON Output

{"field":"value","json.field":"field_value"}

Example: Camel Case - To JSON (first letter lower case)

Example code snippet

  data:
    lo_ajson   type ref to zcl_ajson,
    li_mapping type ref to zif_ajson_mapping.
  data:
    begin of ls_result,
      field_data type string,
    end of ls_result.

  li_mapping = zcl_ajson_mapping=>create_camel_case( iv_first_json_upper = abap_false ).

  ls_result-field_data = 'field_value'.

  lo_ajson = zcl_ajson=>create_empty( ii_custom_mapping = li_mapping ).

  lo_ajson->set( iv_path = '/' iv_val = ls_result ).

JSON Output

{"fieldData":"field_value"}

Example: Camel Case - To JSON (first letter upper case)

Example code snippet

  data:
    lo_ajson   type ref to zcl_ajson,
    li_mapping type ref to zif_ajson_mapping.
  data:
    begin of ls_result,
      field_data type string,
    end of ls_result.

  li_mapping = zcl_ajson_mapping=>create_camel_case( iv_first_json_upper = abap_true ).

  ls_result-field_data = 'field_value'.

  lo_ajson = zcl_ajson=>create_empty( ii_custom_mapping = li_mapping ).

  lo_ajson->set( iv_path = '/' iv_val = ls_result ).

JSON Output

{"FieldData":"field_value"}

Example: Camel Case - To ABAP

JSON Input

{"FieldData":"field_value"}

Example code snippet

  data:
    lo_ajson   type ref to zcl_ajson,
    li_mapping type ref to zif_ajson_mapping.
  data:
    begin of ls_result,
      field_data type string,
    end of ls_result.

  li_mapping = zcl_ajson_mapping=>create_camel_case( ).

  lo_ajson = zcl_ajson=>parse( iv_json = '{"FieldData":"field_value"}' ii_custom_mapping = li_mapping ).

  lo_ajson->to_abap( importing ev_container = ls_result ).

Example: Lower Case - To JSON

Example code snippet

  data:
    lo_ajson   type ref to zcl_ajson,
    li_mapping type ref to zif_ajson_mapping.
  data:
    begin of ls_result,
      field_data type string,
    end of ls_result.

  li_mapping = zcl_ajson_mapping=>create_lower_case( ).

  ls_result-field_data = 'field_value'.

  lo_ajson = zcl_ajson=>create_empty( ii_custom_mapping = li_mapping ).

  lo_ajson->set( iv_path = '/' iv_val = ls_result ).

JSON Output

{"field_data":"field_value"}

Example: Upper Case - To JSON

Example code snippet

  data:
    lo_ajson   type ref to zcl_ajson,
    li_mapping type ref to zif_ajson_mapping.
  data:
    begin of ls_result,
      field_data type string,
    end of ls_result.

  li_mapping = zcl_ajson_mapping=>create_upper_case( ).

  ls_result-field_data = 'field_value'.

  lo_ajson = zcl_ajson=>create_empty( ii_custom_mapping = li_mapping ).

  lo_ajson->set( iv_path = '/' iv_val = ls_result ).

JSON Output

{"FIELD_DATA":"field_value"}

Filtering

This is an experimental feature, the interface may change. filter() method looks more favorable option

This feature allows creating a json from existing one skipping some nodes. E.g. empty values, predefined paths or using your custom filter.

Predefined filters

  • Remove empty values
  " li_json_source: { "a":1, "b":0, "c":{ "d":"" } }
  li_json_filtered = li_json_source->filter( zcl_ajson_filter_lib=>create_empty_filter( ) ).
  " li_json_filtered: { "a":1 }
  " OR ... (but prefer the former)
  li_json_filtered = zcl_ajson=>create_from(
    ii_source_json = li_json_source
    ii_filter = zcl_ajson_filter_lib=>create_empty_filter( ) ).
  • Remove predefined paths
  " li_json_source: { "a":1, "b":0, "c":{ "d":"" } }
  li_json_filtered = li_json_source->filter( 
    zcl_ajson_filter_lib=>create_path_filter(
      it_skip_paths = value #( ( '/b' ) ( '/c' ) )
  ) ).
  " li_json_filtered: { "a":1 }
  
  " OR also
  ...
  zcl_ajson_filter_lib=>create_path_filter( iv_skip_paths = '/b,/c' ).
  ...

... works also with patterns (e.g. to remove meta data attrs)

  zcl_ajson_filter_lib=>create_path_filter( 
    iv_skip_paths = '*/@*'
    iv_pattern_search = abap_true ).
  • compound ("and") filter
  ...
  zcl_ajson_filter_lib=>create_and_filter( value #(
    ( zcl_ajson_filter_lib=>create_empty_filter( ) )
    ( zcl_ajson_filter_lib=>create_path_filter( iv_skip_paths = '/xyz' ) )
  ) ).
  ...

Custom filters

In order to apply a custom filter you have to implement a class with zif_ajson_filter interface. The interface has one method keep_node which receives is_node - json tree node of zif_ajson=>ty_node type and also the iv_visit param. iv_visit will be zif_ajson_filter=>visit_type-value for all final leafs (str,num,bool,null) and will get visit_type-open or visit_type-close values for objects and arrays. So the objects and arrays will be called twice - before and after filtering - this allows examining their children number before and after the current filtering. For example of implementation see local implementations of zcl_ajson_filter_lib class.

  method zif_ajson_filter~keep_node.
    " remove all nodes starting with 'x'
    rv_keep = boolc( is_node-name is initial or is_node-name+0(1) <> 'x' ).
  endmethod.

Utilities

Class zcl_ajson_utilities provides the following methods:

  • new - static method to create an instance (the shortcut for pre 7.4 abap releases)
  • diff - returns all inserts, deletions, and changes between two JSON objects
  • sort - returns JSON string with nodes sorted alphabetically
  • is_equal - returns true if 2 jsons (or json string) are deeply equal
  • merge - merges 2 jsons together

Difference between JSON

The delta between two JSON objects or strings is returned as three JSON objects containing nodes that where inserted, deleted, or changed.

Notes:

  • In case the type of a node changes, it is returned as a deletion of the old node and an insert of the new node (since arrays or objects could be involved).
  • The order of nodes is irrelevant for the comparison.
  data:
    lo_util       type ref to zcl_ajson_utilities,
    lv_original   type string,
    lv_comparison type string,
    lo_insert     type ref to zcl_ajson,
    lo_delete     type ref to zcl_ajson,
    lo_change     type ref to zcl_ajson.

  lv_original = '{"a":1,"b":"B","e":[],"f":[5]}'.
  
  lv_comparison = '{"a":2,"c":"C","e":[1],"f":[4]}'.
  
  create object lo_util.

  lo_util->diff(
    exporting
      iv_json_a = lv_original
      iv_json_b = lv_comparison
    importing
      eo_insert = lo_insert
      eo_delete = lo_delete
      eo_change = lo_change ).

  " lo_insert
  " {"c":"C","e":[1]}
  " lo_delete
  " {"b":"B"}
  " lo_change
  " {"a":2,"f":[5]}

You can see a more complex example in the test class of zcl_ajson_utilities.

Sorting of JSON object or string

  data:
    lo_util type ref to zcl_ajson_utilities,
    lv_original type string,
    lv_sorted type string.
    
  lv_original = '{"e":[],"b":"B","f":[5],"a":1}'.

  create object lo_util.

  lv_sorted = lo_util->sort( iv_json = lv_original ).
  " {
  "   "a": 1,
  "   "b": "B",
  "   "e": [],
  "   "f": [
  "     5
  "   ]
  " }

Testing equality

  zcl_ajson_utilities=>new( )->is_equal(
    iv_json_a = '{"a":1,"b":2}'
    iv_json_b = '{"a":1,"b":2}' ).       " Return abap_true
  zcl_ajson_utilities=>new( )->is_equal(
    iv_json_a = '{"a":1,"b":2}'
    iv_json_b = '{"a":1,"b":2,"c":3}' ). " Return abap_false

Other

Checking current instance behavior options

Behavior options like read_only or keep_item_order are accessible via opts() method (returns zif_ajson=>ty_opts).

Known issues

  • removing an array item in the middle of array will not renumber the items

References

ajson's People

Contributors

albertmink avatar christianguenter2 avatar cwildt avatar jrodriguez-rc avatar jxysgx7c avatar larshp avatar mathieulawnik avatar mbtools avatar oblomov-dev avatar postavka avatar sbcgua avatar schneidermic0 avatar thorsten-wolf-neptune avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ajson's Issues

Bug: set(array item)

  • should not set with non-numeric key, maybe even force sequncial number (or maybe not)
  • add index if new item is created
  • add UTs for all of that

License

Maybe add a license which makes it easier to reference for someone (me ;-) ) who wants to reuse this lovely code

array column names are sorted

Hi,
Should the behavior be changed, so the json follows the ABAP structure? Or add an option to keep the columns/structure fields in the same order?

image

image

image

reader: rename value to get ?

Maybe rename value* methods to get* to match setters in the writer. And exists to has. While it is not used a lot

Diff

I implemented a diff including test case.

mbtools@d1502b0

The current use case is to find out what changed in abaplint.json or what's the difference to the default abaplint.json.

Issues:

  • It's a bit too much code since "set" does not take "type" from the JSON node as input and I had to repeat the set logic 3x.
  • "set" will always create the path using type "object". How do I set a node to be "array"?
  • The "save as..." part of the code could be much simpler if we had a "set_with_type" method to insert directly without interpreting the type of "iv_val" (meaning iv_val and iv_type match the internal node value).
eo_change->set_with_type( iv_path         = <ls_node_b>-path && <ls_node_b>-name && '/'
                          iv_val          = <ls_node_b>-value
                          iv_type         = <ls_node_b>-type
                          iv_ignore_empty = iv_ignore_empty ).

Probably needs some tuning/testing like dealing with empty arrays.

Let me know if you want a PR.

replace use of XFELD

with one of the types from #74

the check for XFELD will be kept in the code, but not actively unit tested, this will make the code one step closer to compatibility with Steampunk and open-abap

any thoughts?

replace DESCRIBE FIELD with RTTI?

as the statement does not work in Steampunk,

./src/zcl_ajson.clas.locals_imp.abap[604, 9]  - Statement does not exist in ABAPCloud(or a parser error), "describe" (parser_error) [E]
./src/zcl_ajson.clas.locals_imp.abap[698, 7]  - Statement does not exist in ABAPCloud(or a parser error), "describe" (parser_error) [E]

Converting hashed tables from JSON to ABAP fails

Example code that fails to convert into ABAP type.

TYPES:
  BEGIN OF ty_parameter,
    technical_id TYPE c LENGTH 4,
    name         TYPE string,
  END OF ty_parameter,
  ty_parameters TYPE HASHED TABLE OF ty_parameter WITH UNIQUE KEY technical_id
    WITH UNIQUE HASHED KEY xml_name COMPONENTS name.
DATA ls_result TYPE ty_parameters.
DATA json TYPE string.
json = '[{"technicalId": "1"}]'.
DATA(lo_ajson) = zcl_abapgit_ajson=>parse( iv_json =   json
                                     ii_custom_mapping = zcl_abapgit_ajson_mapping=>create_camel_case( ) ).
lo_ajson->to_abap( IMPORTING ev_container = ls_result ).

Incorrect path when using CamelCase

Camel case does not work correctly for nested structures.

REPORT ztest_ajson_camel.

TYPES:
  BEGIN OF ty_snap_simple,
    code      TYPE string,
    file_name TYPE string,
    BEGIN OF profile_info,
      show_fullname TYPE abap_bool,
      position      TYPE string,
    END OF profile_info,
  END OF ty_snap_simple.

DATA:
  ls_json    TYPE ty_snap_simple,
  li_mapping TYPE REF TO /mbtools/if_ajson_mapping,
  lo_json    TYPE REF TO /mbtools/if_ajson,
  lv_data    TYPE string.

ls_json-code                       = 'REPORT zfoobar.\n'.
ls_json-file_name                  = 'test.abap'.
ls_json-profile_info-show_fullname = abap_true.
ls_json-profile_info-position      = 'bottom-center'.

TRY.
    li_mapping = /mbtools/cl_ajson_mapping=>create_camel_case( iv_first_json_upper = abap_false ).

    lo_json = /mbtools/cl_ajson=>create_empty( ii_custom_mapping = li_mapping ).

    lo_json->keep_item_order( ).
    lo_json->set(
      iv_path = '/'
      iv_val  = ls_json ).

    lv_data = lo_json->stringify( 2 ).
  CATCH cx_root.
    BREAK-POINT.
ENDTRY.

WRITE lv_data.
{  
  "code": "REPORT zfoobar.",
 "fileName": "test.abap",
 "profileInfo": {
  }
}

I will submit PR

Change converter/mapper application moment

followup from #110, to think and maybe adjust the behavior

at the parsing step the json is parsed and the mapper is assigned after. Maybe it's wrong. Maybe I'd expect the camel case converted into snake (abap-like) - field_data

the conversion should happen only on in/out ? Like parsing/serialization ? And the set should keep the name as is ?

CC: @jrodriguez-rc @albertmink

Performance tracking

For performance history. Maybe needs a better place.

So first result, NW752, Intel i7-8565U, 32GB

AJSON simple performance test
Date 2021-12-07
PARSE_PLAIN_OBJ                rounds: 1000         result: 0.1352210
PARSE_DEEP_OBJ                 rounds: 1000         result: 0.2143690
PARSE_ARRAY                    rounds: 1000         result: 0.4908800
PARSE_LONG_ARRAY               rounds: 5            result: 6.4535280
PARSE_COMPLEX                  rounds: 5            result: 4.5247740
TO_ABAP_PLAIN_OBJ              rounds: 1000         result: 0.0344460
TO_ABAP_DEEP_OBJ               rounds: 1000         result: 0.0763780
TO_ABAP_ARRAY                  rounds: 1000         result: 0.2096340
TO_ABAP_LONG_ARRAY             rounds: 5            result: 2.8247510
TO_ABAP_COMPLEX                rounds: 5            result: 2.1266610

make format_datetime the default behavior ?

Usual json date format is "YYYY-MM-DD", yet by default ajson keeps the abap formatting (no dashes). This is fixed by the format_datetime call at creation time. Though ... maybe it makes sense to make date formatting the default behavior ...

Merge interfaces

create zif_ajson to describe all methods of the class (merge reader and writer) + freeze + types. The latter can be a breaking change.
reader and writer will stay for now

follow up from #37

Unserialize json strings

I have JSON API responses that contain serialized objects (often when PHP is on the other end).
https://www.php.net/manual/en/function.serialize.php

Example with array with 2 key-value pairs of type string:

{
        "success": true,
	"icons": "a:2:{s:2:\"1x\";s:15:\"pic_128x128.jpg\";s:2:\"2x\";s:15:\"pic_256x256.jpg\";}"
}

Here's the logic of how data is serialized:
https://www.php.net/manual/en/function.serialize.php#66147

Would be a great enhancement to include an unserialize function.

Auto camel case?

ideas:

  • autoConvertCamelCase( true/false )
  • reader->to_abap( 'abcXyz' ) => abc_xyz
  • writer->set( structure ) -> abcXyz
  • writer->push

Aliases for interfaces

Maybe add aliases for interface methods. Not sure. Interfaces can be useful to separate concerns, but for some usages it is inconvenient and bulky to reassign or prefix them

    aliases:
      exists for zif_ajson_reader~exists,
      members for zif_ajson_reader~members,
      value for zif_ajson_reader~value,
      value_boolean for zif_ajson_reader~value_boolean,
      value_integer for zif_ajson_reader~value_integer,
      value_number for zif_ajson_reader~value_number,
      value_string for zif_ajson_reader~value_string,
      slice for zif_ajson_reader~slice,
      to_abap for zif_ajson_reader~to_abap.

    aliases:
      clear for zif_ajson_writer~clear,
      set for zif_ajson_writer~set,
      set_boolean for zif_ajson_writer~set_boolean,
      set_string for zif_ajson_writer~set_string,
      set_integer for zif_ajson_writer~set_integer,
      set_date for zif_ajson_writer~set_date,
      delete for zif_ajson_writer~delete,
      touch_array for zif_ajson_writer~touch_array,
      push for zif_ajson_writer~push.

unused variables

found by abaplint rule unused_variables, which is currently disabled for this repo, I checked all 7 findings and it looks like its correct,

./src/zcl_ajson_mapping.clas.locals_imp.abap[68, 19]  - Variable "<token>" not used (unused_variables) [E]
./src/zcl_ajson_mapping.clas.locals_imp.abap[104, 19] - Variable "<token>" not used (unused_variables) [E]
./src/zcl_ajson.clas.abap[487, 10]                    - Variable "lt_path" not used (unused_variables) [E]
./src/zcl_ajson.clas.abap[491, 19]                    - Variable "<topnode>" not used (unused_variables) [E]
./src/zcl_ajson.clas.locals_imp.abap[757, 9]          - Variable "io_type" not used (unused_variables) [E]
./src/zcl_ajson.clas.testclasses.abap[1467, 16]       - Variable "gv_sample" not used (unused_variables) [E]
./src/zcl_ajson.clas.testclasses.abap[1906, 10]       - Variable "lo_nodes_exp" not used (unused_variables) [E]

set_string with custom_mapping does rename field

Intention is to override JSON data. We read the JSON data, then calculate a new value which then shall be stored in the JSON data.

When setting a string value, its fieldname is changed. Here from fieldData to fielddata. Is this a bug?

DATA ajson   TYPE REF TO zcl_ajson.
DATA path TYPE string.

ajson = zcl_ajson=>parse( iv_json = `{"fieldData":"field_value"}`
                          ii_custom_mapping = zcl_ajson_mapping=>create_camel_case( iv_first_json_upper = abap_false ) ).

" /fieldata = E
ajson->set_string( iv_path = `/fieldData`  iv_val = 'E' ).

Without custom_mapping the field name stays the same.

Set_string does not keep_item_order

We found that set_string calls set the order of the affected node always to 0. We expected that the set_string does not touch the order of the nodes.

Before the set_string call in line 34 the order is 2
image

and after it is set to 0?
image

Code snippet to reproduce:

TYPES:
  BEGIN OF ty_header_60_src,
    description           TYPE string,
    original_language     TYPE string,
    abap_language_version TYPE string,
  END OF ty_header_60_src,

  BEGIN OF ty_main,
    header TYPE ty_header_60_src,
  END OF ty_main.

DATA:
  ajson TYPE REF TO zcl_ajson,
  data  TYPE ty_main.

data = VALUE #( header = VALUE #( description = 'description'
                                  abap_language_version = 'standard'
                                  original_language = 'E' ) ).

ajson = zcl_ajson=>create_empty( zcl_ajson_mapping=>create_camel_case( iv_first_json_upper = abap_false ) ).
ajson->keep_item_order( ).
ajson->set(
  iv_path = '/'
  iv_val  = data ).


ajson->set_string( iv_path = `/header/originalLanguage`  iv_val = 'en' ).

new feature: get members ordered by field

{
  "results": [
    {
      "key": "def",
      "value": 789
    },
    {
      "key": "abc",
      "value": 456
    },
    {
      "key": "ghi",
      "value": 123
    }
  ]
}
lt_members = lo_json->members( '/results' ).   " returns [1,2,3]

New option would allow to have the members sorted:

lt_members = lo_json->members( iv_path = '/results' iv_sort = "key" ).   " returns [2,1,3]

lt_members = lo_json->members( iv_path = '/results' iv_sort = "value" ).   " returns [3,2,1]

Simplified string version of SET (idea, needs opinions)

For manual structure creation (looks quite typical for various API requests), it could be more readable to use a simplified one-line version of set.

    lo_json->set(
      iv_path = '/a/b'
      iv_val  = 'xyz' ).

vs

lo_json->sets( '/a/b:xyz' ).
" or
lo_json->sets( |/a/b:{ iv_param }| ).
  • s stands for short or string (would like to keep the method name short)
  • type should be probably guessed, e.g.
    • null, true, false, undefined - can be hardcoded
    • 123 - is a number, but "123" is a string (I think edge cases like real string with quotes can be ignored as rare)
    • objects, arrays unsupported ... though theoretically can be parsed ... as it may happen that object has a constant value { "type_id": 12 }
  • params - probably ignore, the value of this methods is to cover 90% of "hardcoded" values, there is always normal set for exceptions.
  • the fact that this method is slower can be ignored, there is always a normal set to the rescue

Any thoughts ?

Item filter

Follows #71 (comment) @larshp

perhaps something like adding this method to the custom mapping interface

methods to_json_skip_value
  importing 
    io_type type ref to cl_abap_typedescr
    is_node type ty_node,
  returning 
    value(rv_skip) type abap_bool.

if it returns rv_skip = abap_true, then skip the value, else as normal

  1. This seems to be not about skipping but about filtering, the implementation may decide to skip filled values too
  2. Thus it doesn't match mapper semantics, it is a separate filter interface
  3. Also because of performance considerations. Mapper without filter will call extra method each time and vice versa
interface zif_ajson_filter " zif_ajson_node_filter ?

  methods filter
   importing 
     io_type type ref to cl_abap_typedescr
     is_node type ty_node,
   returning 
     value(rv_KEEP) type abap_bool.

option to ignore initial values?

I have the following code,

DATA intf TYPE zif_aff_intf_v1=>ty_main.
DATA li_ajson TYPE REF TO zif_abapgit_ajson.
intf-header-description = 'hello world'.
li_ajson = zcl_abapgit_ajson=>create_empty( ).
li_ajson->keep_item_order( ).
li_ajson->set(
  iv_path = '/'
  iv_val  = intf ).
DATA(json) = li_ajson->stringify( 2 ).
BREAK-POINT.

it creates the following json,

{
  "schema": "",
  "header": {
    "description": "hello world",
    "original_language": "",
    "abap_language_version": ""
  },
  "category": "00",
  "proxy": false,
  "descriptions": {
    "types": [],
    "attributes": [],
    "events": [],
    "methods": []
  }
}

I'd like to ignore initial fields, ie. expected outcome is

{
  "header": {
    "description": "hello world"
  }
}

any suggestions/thoughts on implementing this?

Chaining for `set` methods

Maybe make all set* return self for chaining ?

Potential issue - set* are defined in writer, but generally we tend to move to usage of common interface. So set* should return zif_ajson ... beging defined in zif_ajson_writer ... maybe it is ok as a transition ...

@mbtools what do you think about it ? I have some faint memories that we discussed something like this, no ?

Get/set_date

Set_date stores YYYY-MM-DD, but get_date requires '^(\d{4})-(\d{2})-(\d{2})(T|$)' with T or $ at the end (same regex in to_abap).

So you can't get a date that was set before. Maybe set_date should store YYYY-MM-DDT00:00:00Z

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.