Friday, February 2, 2018

Robust ABAP to JSON serializer

SAP provides some classes to serialize and deserialize arbitrary ABAP variables. Unfortunately all of them have some disadvantages. The classes are often to complex to use. One method call should be enough to serialize. It is not necessary to call constructor and multiple methods. But a real problem of most classes is, that they just dump for some data constallations, e.g. if the variable contains json Control character like [, ], { or }. Even the colon can cause Problems.


Since the implementaion is easy, we made our own. I am sure that also our own implementation will have bugs... but we can correct them quickly, what is not possible with the SAP's standard classes


class ZCL_JSO_SERIAL definition
  public
  create public .

public section.
  types:
    BEGIN OF yt_s_component_value .
    TYPES   name TYPE string.
    TYPES   value TYPE string.
    TYPES END OF yt_s_component_value .
  types:
    yt_t_component_value TYPE SORTED TABLE OF yt_s_component_value WITH UNIQUE KEY name .

  class-methods SERIALIZE
    importing
      !I_ABAP type DATA
    returning
      value(R_V_JSON) type STRING .
  class-methods SERIALIZE_CHECK
    importing
      !I_ABAP type DATA
    exporting
      !E_V_JSON type STRING
    returning
      value(R_V_SUCCESS) type ABAP_BOOL .
  class-methods DESERIALIZE
    importing
      !I_V_JSON type STRING
    exporting
      !E_ABAP type DATA
    raising
      CX_XSLT_DESERIALIZATION_ERROR .




protected section.
  class-methods GET_COMPONENTS_OF_STRUCTURE
    importing
      !I_V_JSON type STRING
    returning
      value(R_T_COMPONENT_VALUE) type YT_T_COMPONENT_VALUE
    raising
      CX_XSLT_DESERIALIZATION_ERROR .

  class-methods SPLIT
    importing
      !I_V_JSON type STRING
      !I_V_SPLITTER type CHAR1 default ','
    returning
      value(R_T_PART) type STRING_TABLE .

  class-methods UNPACK
    importing
      !I_V_JSON type STRING
    returning
      value(R_V_JSON) type STRING
    raising
      CX_XSLT_DESERIALIZATION_ERROR .




  METHOD serialize.

    DATA lv_type.
    DATA lv_item_count TYPE i.
    DATA lo_structdescr TYPE REF TO cl_abap_structdescr.
    DATA lv_index TYPE syindex.
    FIELD-SYMBOLS <ls_compdescr> TYPE abap_compdescr.
    FIELD-SYMBOLS <lv_item> TYPE any.
    FIELD-SYMBOLS <lt_abap> TYPE ANY TABLE.

    DESCRIBE FIELD i_abap TYPE lv_type COMPONENTS lv_item_count.

    IF lv_type <> cl_abap_typedescr=>typekind_table.
      IF lv_item_count = 0.
*** unstructured (plain field)
        r_v_json = i_abap. " This assignment can cause problems. Monitor carefully!
        r_v_json = |"{ cl_http_utility=>escape_url( r_v_json ) }"|.
      ELSE.
*** structured
        r_v_json = '{'.
        lo_structdescr = CAST #( cl_abap_typedescr=>describe_by_data( i_abap ) ).
        LOOP AT lo_structdescr->components ASSIGNING <ls_compdescr>.
          lv_index = sy-tabix .
          ASSIGN COMPONENT <ls_compdescr>-name OF STRUCTURE i_abap TO <lv_item> .

          r_v_json = |{ r_v_json }| &&
            |{ to_lower( <ls_compdescr>-name ) }:| &&
            |{ serialize( <lv_item> ) }|.

          IF lv_index < lv_item_count.
            r_v_json = |{ r_v_json },|.
          ENDIF .
        ENDLOOP .
        r_v_json = |{ r_v_json }\}|.
      ENDIF.
    ELSE.
*** internal table
      r_v_json = '['.
      ASSIGN i_abap TO <lt_abap>.
      LOOP AT <lt_abap> ASSIGNING <lv_item>.
        lv_index = sy-tabix .

        r_v_json = |{ r_v_json }{ serialize( <lv_item> ) }|.

        IF lv_index < lines( <lt_abap> ).
          r_v_json = |{ r_v_json },|.
        ENDIF .
      ENDLOOP.
      r_v_json = |{ r_v_json }]|.
    ENDIF.

  ENDMETHOD.




  METHOD deserialize.

    DATA lv_type.
    DATA lv_item_count TYPE i.
    DATA lv_value TYPE string.
    DATA lv_json TYPE string.
    DATA lo_structdescr TYPE REF TO cl_abap_structdescr.
    DATA ls_component TYPE yt_s_component_value.
    DATA lt_component TYPE yt_t_component_value.
    DATA lt_json TYPE string_table.
    DATA lr_row TYPE REF TO data.

    FIELD-SYMBOLS <ls_compdescr> TYPE abap_compdescr.
    FIELD-SYMBOLS <lv_item> TYPE any.
    FIELD-SYMBOLS <lt_abap> TYPE ANY TABLE.
    FIELD-SYMBOLS <l_abap> TYPE any.

    CLEAR e_abap.
    DESCRIBE FIELD e_abap TYPE lv_type COMPONENTS lv_item_count.

    IF lv_type <> cl_abap_typedescr=>typekind_table.
      IF lv_item_count = 0.
*** unstructured
        lv_value = unpack( i_v_json ). " remove quotation marks
        lv_value = cl_http_utility=>unescape_url( lv_value ).

        e_abap = lv_value.
      ELSE. " unstructured i_v_json (plain variable)
*** structured
        " get component descriptor for abap object
        lo_structdescr = CAST #( cl_abap_typedescr=>describe_by_data( e_abap ) ).
        " get list of json components
        lt_component = get_components_of_structure( i_v_json ).
        " iterate components of abap structure
        LOOP AT lo_structdescr->components ASSIGNING <ls_compdescr>.
          " assign json components to components of abap structure
          ASSIGN COMPONENT <ls_compdescr>-name OF STRUCTURE e_abap TO <lv_item> .
          CLEAR ls_component.
          READ TABLE lt_component INTO ls_component
            WITH TABLE KEY name = to_lower( <ls_compdescr>-name ).
          IF sy-subrc = 0.
            deserialize(
              EXPORTING i_v_json = ls_component-value
              IMPORTING e_abap = <lv_item>
            ).
          ELSE.
            CLEAR <lv_item>.
          ENDIF.
        ENDLOOP. " components of structure
      ENDIF. " structured i_v_json
    ELSE.
*** internal table
      " get field symbol for table
      ASSIGN e_abap TO <lt_abap>.

      " create work area for table
      CREATE DATA lr_row LIKE LINE OF <lt_abap>.
      ASSIGN lr_row->* TO <l_abap>.

      " get table of json strings
      lv_json = unpack( i_v_json ). " remove braces [ ]
      lt_json = zcl_jso_serial=>split( lv_json ). " split at comma

      " iterate json strings and deserialize each
      LOOP AT lt_json INTO lv_json.
        deserialize(
          EXPORTING i_v_json = lv_json
          IMPORTING e_abap = <l_abap>
        ).
        INSERT <l_abap> INTO TABLE <lt_abap>. " collect result
      ENDLOOP.
    ENDIF. " i_v_json contains data of internal table

  ENDMETHOD.




  METHOD get_components_of_structure.

    DATA lv_name TYPE string.
    DATA lv_value TYPE string.
    DATA lv_s TYPE string.
    DATA lt_part TYPE string_table.

    " remove braces { }
    lv_s = unpack( i_v_json ).

    " split at comma
    lt_part = zcl_jso_serial=>split( lv_s ).

    " split name and value at colon
    LOOP AT lt_part INTO lv_s.
      SPLIT lv_s AT ':' INTO lv_name lv_value.
      INSERT VALUE #( name = lv_name  value = lv_value ) INTO TABLE r_t_component_value.
    ENDLOOP.

  ENDMETHOD.




  METHOD split.

    DATA lv_i TYPE i.
    DATA lv_start TYPE i.
    DATA lv_len TYPE i.
    DATA lv_c.
    DATA lv_b1_count TYPE i.
    DATA lv_b2_count TYPE i.

    CHECK strlen( i_v_json ) > 0.

    lv_i = 0.
    lv_start = 0.
    lv_len = 0.
    WHILE lv_i < strlen( i_v_json ).
      lv_c = i_v_json+lv_i(1).
      ADD 1 TO lv_len.

      IF lv_c = '['. ADD 1 TO lv_b1_count. ENDIF.
      IF lv_c = ']'. SUBTRACT 1 FROM lv_b1_count. ENDIF.
      IF lv_c = '{'. ADD 1 TO lv_b2_count. ENDIF.
      IF lv_c = '}'. SUBTRACT 1 FROM lv_b2_count. ENDIF.

      IF ( lv_c = i_v_splitter OR lv_i = strlen( i_v_json ) - 1 ) AND lv_b1_count = 0 AND lv_b2_count = 0.
        IF lv_i < strlen( i_v_json ) - 1. SUBTRACT 1 FROM lv_len. ENDIF.
        INSERT substring( val = i_v_json  off = lv_start  len = lv_len ) INTO TABLE r_t_part.
        lv_start = lv_i + 1.
        lv_len = 0.
      ENDIF.

      ADD 1 TO lv_i.
    ENDWHILE.

  ENDMETHOD.




  METHOD unpack.

    DATA lv_first.
    DATA lv_last.

    " validity checks
    IF strlen( i_v_json ) < 2.
      RAISE EXCEPTION TYPE cx_xslt_deserialization_error.
    ENDIF.

    " get first and last character
    lv_first = i_v_json(1).
    lv_last  = substring( val = i_v_json off = strlen( i_v_json ) - 1 len = 1 ).

    IF ( lv_first = '"' AND lv_last = '"' ) OR
       ( lv_first = '[' AND lv_last = ']' ) OR
       ( lv_first = '{' AND lv_last = '}' ).
      " unpack
      r_v_json = substring( val = i_v_json off = 1 len = strlen( i_v_json ) - 2 ).
    ELSE.
      RAISE EXCEPTION TYPE cx_xslt_deserialization_error.
    ENDIF.

  ENDMETHOD.




  METHOD serialize_check.

    DATA lr_abap TYPE REF TO data.

    FIELD-SYMBOLS <l_abap> TYPE any.

    " create a second variable <l_abap> with the same type like i_abap
    CREATE DATA lr_abap LIKE i_abap.
    ASSIGN lr_abap->* TO <l_abap>.

  TRY. 
  " serialize i_abap to e_v_json
    e_v_json = serialize( i_abap ).

    " deserialize e_v_json to <l_abap>
    deserialize(
      exporting i_v_json = e_v_json
      importing e_abap   = <l_abap>
    ).
    CATCH cx_xslt_deserialization_error.
      r_v_success = abap_false.
      RETURN.
  ENDTRY.


    " now i_abap and <l_abap> shall be equal
    IF i_abap = <l_abap>.
      r_v_success = abap_true.
    ELSE.
      r_v_success = abap_false.
    ENDIF.

  ENDMETHOD.

No comments:

Post a Comment

Working with the session Id // ID für den Modus

Sometimes it can be helpful to know the id of the current session     CALL FUNCTION 'TH_GET_CONTEXT_ID'       IMPORTING         cont...