Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
CLAUDE.md
.claude/
kb/
*.log
562 changes: 292 additions & 270 deletions docs/faq.md

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions docs/feature-requests.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,40 @@ DATA lt_results TYPE STANDARD TABLE OF ts_result WITH DEFAULT KEY.

---

#### Implementation evaluation (2026-07-23) — target: next patch level, all classes

**Scope decision**: implement in all three parsing entry points so the API stays uniform across editions: `Z_UI2_JSON`, `/UI2/CL_JSON` (string-offset parser), and `Z_UI2_JSON2` (kernel `IF_JSON_READER`). The API surface (a new optional `PATH` importing parameter) is identical; only the internal navigation differs.

**API shape** (per the narrow-static-API rule this *can* go on the static methods — it is a common one-off need, not an advanced switch, and it does not depend on constructor state):
```abap
class-methods DESERIALIZE
importing ... !PATH type STRING optional ...
```
Same addition on `GENERATE`. No change to `CONSTRUCTOR` or the `*_INT` instance methods' core loop — path resolution is a pre-positioning step that runs once before the existing `restore_type` / `generate_int` recursion begins.

**Where it hooks in — `Z_UI2_JSON` / `/UI2/CL_JSON` (offset parser):**
- `deserialize_int` (`src/z_ui2_json.clas.abap:674`) positions `offset` at the first structural char via `while_offset_not_cs`, then calls `restore_type`.
- A path pre-step would, before that call, walk the object levels named in `PATH`: at each segment `eat_char '{'` → loop `eat_name` / `eat_white` / `eat_char ':'`, comparing the key to the segment; on match, descend; on miss, skip the value. Skipping an unwanted value already exists — `restore_type` called **without** `data` supplied consumes and discards a value (`src/z_ui2_json.clas.abap:2040`, `:2196`), so the skip logic is reusable, not new code.
- After the final segment is matched and `offset` sits on the subnode's opening char, hand off to the existing `restore_type( ... data = data ... )` unchanged.

**Where it hooks in — `Z_UI2_JSON2` (kernel reader):**
- `deserialize_int` (`src/z_ui2_json2.clas.abap:494`) does `lo_reader->next_node( )` then `restore_type`.
- Path navigation is cleaner here: walk `reader->node-name` at each `open_object` level (mirrors the existing loop at `src/z_ui2_json2.clas.abap:1301`), calling `reader->skip_node( )` for non-matching members (same primitive the WA2 skip_node fix relies on) until the target segment is reached, then hand off to `restore_type`.

**Performance — when `PATH` is NOT supplied (the hot path, must stay neutral):**
- Guard with a single `IF path IS NOT INITIAL.` around the entire pre-step. When empty, the added cost is one `IS INITIAL` test per top-level `deserialize`/`generate` call — not per node, not per field. This is immeasurable against the existing per-call setup (RTTI describe, `while_offset_not_cs` BOM scan).
- **Requirement**: the path-splitting regex/`SPLIT` must run **only inside** the `path IS NOT INITIAL` branch. Do not compile or split at construction time. Confirm the `Z_UI2_JSON_PERF` baseline scenarios are unchanged (target: 0% delta; anything >5% on a previously-neutral scenario blocks the change per CLAUDE.md).

**Performance — when `PATH` IS supplied:**
- Net cost is *sub-linear in the skipped volume vs. the current workaround*: today the wrapper-structure approach parses AND type-converts the outer envelope; path-skip parses the envelope tokens but does **no** RTTI lookup or MOVE for skipped members. So the feature is faster than the workaround it replaces, not just more convenient.
- One-time cost: split `PATH` into segments (bounded, tiny — typically 1–3 segments). Reuse the `Z_UI2_DATA_ACCESS` `so_regex_hier` pattern (`src/z_ui2_data_access.clas.abap:307`) only if array indexing is in scope; for the object-member-only v1, a plain `SPLIT path AT '-'` is cheaper and sufficient.

**Scope for v1 (recommended)**: object-member traversal only (no `[n]` indexing). Covers the OData `d-results` canonical case. Array indexing deferred to a follow-up — it complicates the offset parser's skip logic (must count array elements) with little added demand.

**Open question**: behavior when a path segment is not found. Options: (a) return initial/unchanged `data` silently, (b) raise `CX_SY_MOVE_CAST_ERROR` with the missing segment in `source_typename`, gated on `STRICT_MODE`. Recommend (b)-under-strict / (a)-otherwise, matching the existing strict-mode contract.

---

### 1.3 Strict-on-unknown-fields

**Status**: Not implemented. Today, `STRICT_MODE = abap_true` raises `CX_SY_MOVE_CAST_ERROR` only on type mismatches; JSON keys with no matching ABAP component are silently ignored regardless of strict mode.
Expand Down
9 changes: 9 additions & 0 deletions src/z_ui2_json.clas.abap
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ protected section.
utclong TYPE abap_typekind VALUE 'p' ##NO_TEXT, " CL_ABAP_TYPEDESCR=>TYPEKIND_UTCLONG -> 'p' only from 7.54
int8 TYPE abap_typekind VALUE '8' ##NO_TEXT, " CL_ABAP_TYPEDESCR=>TYPEKIND_INT8 -> '8' only from 7.40
enum TYPE abap_typekind VALUE 'k' ##NO_TEXT, " CL_ABAP_TYPEDESCR=>TYPEKIND_ENUM -> 'k'
decfloat16 TYPE abap_typekind VALUE 'a' ##NO_TEXT, " CL_ABAP_TYPEDESCR=>TYPEKIND_DECFLOAT16 -> 'a' only from 7.40
decfloat34 TYPE abap_typekind VALUE 'e' ##NO_TEXT, " CL_ABAP_TYPEDESCR=>TYPEKIND_DECFLOAT34 -> 'e' only from 7.40

" just aliasing
float TYPE abap_typekind VALUE cl_abap_typedescr=>typekind_float,
Expand Down Expand Up @@ -1098,6 +1100,13 @@ CLASS Z_UI2_JSON IMPLEMENTATION.
ELSE.
r_json = data.
ENDIF.
WHEN e_typekind-decfloat16 OR e_typekind-decfloat34.
IF data IS INITIAL.
r_json = `0`.
ELSE.
r_json = data.
CONDENSE r_json.
ENDIF.
WHEN e_typekind-int OR e_typekind-int1 OR e_typekind-int2 OR e_typekind-packed OR e_typekind-int8.
IF data IS INITIAL.
r_json = `0`.
Expand Down
7 changes: 7 additions & 0 deletions src/z_ui2_json.clas.macros.abap
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,13 @@ DEFINE dump_type_int.
ELSE.
&3 = &1.
ENDIF.
WHEN z_ui2_json=>e_typekind-decfloat16 OR z_ui2_json=>e_typekind-decfloat34.
IF &1 IS INITIAL.
&3 = `0`.
ELSE.
&3 = &1.
CONDENSE &3.
ENDIF.
WHEN e_typekind-int OR e_typekind-int1 OR e_typekind-int2 OR e_typekind-packed OR e_typekind-int8.
IF &1 IS INITIAL.
&3 = `0`.
Expand Down
31 changes: 31 additions & 0 deletions src/z_ui2_json.clas.testclasses.abap
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ INHERITING FROM z_ui2_json.
METHODS deser_field_invalid_value FOR TESTING.
"! serialized timestamps with domain XSDDATETIME_Z
METHODS serialize_time_stamp FOR TESTING.
"! DECFLOAT16/DECFLOAT34 zero must serialize as 0, not null
METHODS serialize_decfloat FOR TESTING.

ENDCLASS. "abap_unit_testclass
* ----------------------------------------------------------------------
Expand Down Expand Up @@ -3062,4 +3064,33 @@ CLASS abap_unit_testclass IMPLEMENTATION.

ENDMETHOD.

METHOD serialize_decfloat.
" DECFLOAT16/DECFLOAT34 zero value must serialize as 0, not null (bug fix)

DATA: lv_d16 TYPE decfloat16,
lv_d34 TYPE decfloat34,
lv_d16_nz TYPE decfloat16,
lv_d34_nz TYPE decfloat34,
lv_json TYPE string.

" zero (initial) values
lv_json = serialize( data = lv_d16 ).
cl_abap_unit_assert=>assert_equals( exp = `0` act = lv_json msg = 'DECFLOAT16 zero must serialize as 0' ).

lv_json = serialize( data = lv_d34 ).
cl_abap_unit_assert=>assert_equals( exp = `0` act = lv_json msg = 'DECFLOAT34 zero must serialize as 0' ).

" non-zero values
lv_d16_nz = '3.14' ##LITERAL.
lv_json = serialize( data = lv_d16_nz ).
cl_abap_unit_assert=>assert_not_initial( act = lv_json msg = 'DECFLOAT16 non-zero must serialize to non-empty' ).
cl_abap_unit_assert=>assert_differs( act = lv_json exp = `null` msg = 'DECFLOAT16 non-zero must not serialize as null' ).

lv_d34_nz = '2.718' ##LITERAL.
lv_json = serialize( data = lv_d34_nz ).
cl_abap_unit_assert=>assert_not_initial( act = lv_json msg = 'DECFLOAT34 non-zero must serialize to non-empty' ).
cl_abap_unit_assert=>assert_differs( act = lv_json exp = `null` msg = 'DECFLOAT34 non-zero must not serialize as null' ).

ENDMETHOD.

ENDCLASS. "abap_unit_testclass
89 changes: 13 additions & 76 deletions src/z_ui2_json2.clas.locals_imp.abap
Original file line number Diff line number Diff line change
Expand Up @@ -274,83 +274,20 @@ CLASS lcl_util IMPLEMENTATION.
ENDMETHOD.

METHOD read_json_to_string.
" Workaround: IF_JSON_READER=>skip_node( writer ) does not work correctly
" on member positions mid-document. Replace with skip_node( writer ) once fixed.
" skip_node( writer ) fails on named nodes — envelope workaround:
" wrap in a temporary object, skip, then strip prefix/suffix via string arithmetic.
DATA(lo_writer) = cl_json_string_writer=>create( ).
DATA(lv_depth) = 0.
DATA lv_is_member TYPE c LENGTH 64.
DO.
CASE reader->node-type.
WHEN if_json_node=>open_object.
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->open_member( reader->node-name ).
lv_is_member+lv_depth(1) = 'X'.
ELSE.
lv_is_member+lv_depth(1) = ' '.
ENDIF.
lo_writer->open_object( ).
lv_depth = lv_depth + 1.
WHEN if_json_node=>close_object.
lo_writer->close_object( ).
lv_depth = lv_depth - 1.
IF lv_is_member+lv_depth(1) = 'X'.
lo_writer->close_member( ).
ENDIF.
IF lv_depth = 0. EXIT. ENDIF.
WHEN if_json_node=>open_array.
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->open_member( reader->node-name ).
lv_is_member+lv_depth(1) = 'X'.
ELSE.
lv_is_member+lv_depth(1) = ' '.
ENDIF.
lo_writer->open_array( ).
lv_depth = lv_depth + 1.
WHEN if_json_node=>close_array.
lo_writer->close_array( ).
lv_depth = lv_depth - 1.
IF lv_is_member+lv_depth(1) = 'X'.
lo_writer->close_member( ).
ENDIF.
IF lv_depth = 0. EXIT. ENDIF.
WHEN if_json_node=>string.
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->open_member( reader->node-name ).
ENDIF.
lo_writer->write_string( reader->node-value ).
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->close_member( ).
ENDIF.
WHEN if_json_node=>number.
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->open_member( reader->node-name ).
ENDIF.
lo_writer->write_number( reader->node-value ).
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->close_member( ).
ENDIF.
WHEN if_json_node=>boolean.
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->open_member( reader->node-name ).
ENDIF.
lo_writer->write_boolean( reader->node-value ).
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->close_member( ).
ENDIF.
WHEN if_json_node=>null.
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->open_member( reader->node-name ).
ENDIF.
lo_writer->write_null( ).
IF reader->node-name IS NOT INITIAL AND lv_depth > 0.
lo_writer->close_member( ).
ENDIF.
WHEN OTHERS.
EXIT.
ENDCASE.
reader->next_node( ).
ENDDO.
rv_json = CAST cl_json_string_writer( lo_writer )->get_json( ).
IF reader->node-name IS NOT INITIAL.
lo_writer->open_object( ).
reader->skip_node( lo_writer ).
lo_writer->close_object( ).
DATA(lv_raw) = CAST cl_json_string_writer( lo_writer )->get_json( ).
DATA(lv_off) = strlen( reader->node-name ) + 4.
rv_json = substring( val = lv_raw off = lv_off len = strlen( lv_raw ) - lv_off - 1 ).
ELSE.
reader->skip_node( lo_writer ).
rv_json = CAST cl_json_string_writer( lo_writer )->get_json( ).
ENDIF.
ENDMETHOD.

ENDCLASS. "lcl_util IMPLEMENTATION
Expand Down
47 changes: 47 additions & 0 deletions src/z_ui2_json2.clas.testclasses.abap
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ INHERITING FROM z_ui2_json2.
METHODS deser_field_invalid_value FOR TESTING.
"! serialized timestamps with domain XSDDATETIME_Z
METHODS serialize_time_stamp FOR TESTING.
"! WA2 for Bug 1: skip_node on named member via envelope object
METHODS skip_node_named_member FOR TESTING.

ENDCLASS. "abap_unit_testclass
* ----------------------------------------------------------------------
Expand Down Expand Up @@ -3067,4 +3069,49 @@ CLASS abap_unit_testclass IMPLEMENTATION.

ENDMETHOD.

METHOD skip_node_named_member.
" Bug 1 / WA2: skip_node( writer ) fails on named member positions.
" Stefan Bresch's workaround: envelope the call in a temporary object,
" then strip the prefix/suffix using string arithmetic.
DATA: lv_json TYPE string,
lv_result TYPE string,
lo_writer TYPE REF TO if_json_writer,
lo_reader TYPE REF TO if_json_reader.

lv_json = '{"outer":{"inner":{"key":"value"}}}'.

" --- Case 1: named member node — WA2 envelope technique ---
lo_reader = cl_json_string_reader=>create( lv_json ).
lo_reader->next_node( ). " open_object (outer)
lo_reader->next_node( ). " open_object (inner), node-name = "outer"
lo_reader->next_node( ). " open_object (key/value), node-name = "inner"

lo_writer = cl_json_string_writer=>create( ).
IF lo_reader->node-name IS NOT INITIAL.
lo_writer->open_object( ).
lo_reader->skip_node( lo_writer ).
lo_writer->close_object( ).
DATA(lv_raw) = CAST cl_json_string_writer( lo_writer )->get_json( ).
DATA(lv_off) = strlen( lo_reader->node-name ) + 4.
DATA(lv_len) = strlen( lv_raw ) - lv_off - 1.
lv_result = substring( val = lv_raw off = lv_off len = lv_len ).
ENDIF.
cl_abap_unit_assert=>assert_equals(
exp = '{"key":"value"}'
act = lv_result
msg = 'WA2: named member subtree not captured correctly' ).

" --- Case 2: unnamed root node — plain skip_node still works ---
lv_json = '{"key":"value"}'.
lo_reader = cl_json_string_reader=>create( lv_json ).
lo_reader->next_node( ). " open_object, node-name IS INITIAL
lo_writer = cl_json_string_writer=>create( ).
lo_reader->skip_node( lo_writer ).
cl_abap_unit_assert=>assert_equals(
exp = '{"key":"value"}'
act = CAST cl_json_string_writer( lo_writer )->get_json( )
msg = 'Direct skip_node on unnamed root should work unchanged' ).

ENDMETHOD.

ENDCLASS. "abap_unit_testclass