From 2819d4cc980b5966200b57ea19fe0e37cb0c01cf Mon Sep 17 00:00:00 2001 From: Alexey Arsenyev <30653201+alexey-arseniev@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:42:49 +0200 Subject: [PATCH 1/5] fix(z_ui2_json2): simplify skip_node workaround using envelope technique skip_node( writer ) fails when the reader is positioned on a named node. Workaround: wrap in a temporary envelope object, call skip_node, then strip the {"name": prefix and trailing } via string arithmetic. Replaces the previous 70-line manual tree-walker with 15 lines. Adds unit test SKIP_NODE_NAMED_MEMBER covering named and unnamed nodes. --- docs/faq.md | 562 +++++++++++++------------- src/z_ui2_json2.clas.locals_imp.abap | 89 +--- src/z_ui2_json2.clas.testclasses.abap | 47 +++ 3 files changed, 352 insertions(+), 346 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 5a68ce4..e844a1b 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,270 +1,292 @@ -# FAQ - -* [It is slow](#it-is-slow) -* [GENERATE or DESERIALIZE into REF TO DATA vs. DESERIALIZE into a typed data structure](#generate-or-deserialize-into-ref-to-data-vs-deserialize-into-a-typed-data-structure) -* [JSON to ABAP data type conversion when using GENERATE or DESERIALIZE into REF TO DATA](#json-to-abap-data-type-conversion-when-using-generate-or-deserialize-into-ref-to-data) -* [Timestamp is not deserialized after PL22](https://github.com/SAP/abap-to-json/blob/main/docs/faq.md#timestamp-is-not-deserialized-after-pl22) -* [Serialize huge data objects into JSON and short dumps](#serialize-huge-data-objects-into-json-and-short-dumps) -* [Encoding of Unicode characters (for example, Chinese)](#encoding-of-unicode-characters-for-example-chinese) -* [Incompatible change for initial date/time fields serializing with PL16](#incompatible-change-for-initial-datetime-fields-serializing-with-pl16) -* [Is there a way to deserialize objects that have references to an Interface?](#is-there-a-way-to-deserialize-objects-that-have-references-to-an-interface) -* [Is it possible to have a defined order of fields in ABAP structures generated when deserializing into REF TO DATA fields? Is it possible to have the fields in the generated structure in the same order as in the JSON file?](#is-it-possible-to-have-a-defined-order-of-fields-in-abap-structures-generated-when-deserializing-into-ref-to-data-fields-is-it-possible-to-have-the-fields-in-the-generated-structure-in-the-same-order-as-in-the-json-file) -* [Is it possible to display the currency amount (CURR fields) formatted in the JSON output based on the related currency (CUKY field)?](#is-it-possible-to-display-the-currency-amount-curr-fields-formatted-in-the-json-output-based-on-the-related-currency-cuky-field) -* [My fields are NOT serialized as true/false, instead, serialized like 'X' or ''! E.g., how to control ABAP/JSON Boolean conversion?](#my-fields-are-not-serialized-as-truefalse-instead-serialized-like-x-or--eg-how-to-control-abapjson-boolean-conversion) -* [I can not use /UI2/CL_JSON for ABAP Cloud BADi development](#i-can-not-use-ui2cl_json-for-abap-cloud-badi-development) -* [You get a short dump OBJECTS_NOT_CHAR when serializing data with enabled conversion exits](#you-get-a-short-dump-objects_not_char-when-serializing-data-with-enabled-conversion-exits) -* [Why are special characters in JSON attribute names not escaped or unescaped?](#why-are-special-characters-in-json-attribute-names-not-escaped-or-unescaped) -* [How to define receiving structures for my JSON?](#how-to-define-receiving-structures-for-my-json) -* [How do I know if DESERIALIZE succeeded?](#how-do-i-know-if-deserialize-succeeded) -* [How do I access fields in data returned by GENERATE?](#how-do-i-access-fields-in-data-returned-by-generate) -* [Why does deserialization silently return empty results for my ABAP object?](#why-does-deserialization-silently-return-empty-results-for-my-abap-object) -* [Data is lost or corrupted after a DESERIALIZE / SERIALIZE round-trip](#data-is-lost-or-corrupted-after-a-deserialize--serialize-round-trip) - -## It is slow -It is as fast as possible to achieve, and it is already heavily optimized in pure ABAP. If you have suggestions on how to make it faster, we welcome them. Features like type conversions, type detections, renaming, data generation, etc, require processing time, and even if they are not active, you may pay the penalty because the class design allows this feature. Operations on strings are not fast in ABAP, and method calls are costly, which is why macros are used within the class. However, the class is robust and can handle any data type for serialization and deserialization, offering many convenient functions that would otherwise need to be implemented manually. It performs well in numerous use cases. - -In scenarios where extensive functionality and flexibility are unnecessary, such as with simple, flat tables requiring fast JSON serialization/deserialization, tweaking /ui2/cl_json for speed by disabling flags is not viable. No other universal classes with comparable functionality are known to be faster. The best solutions currently are: - -* **CALL TRANSFORMATION id**: This is the fastest alternative (10x), but it requires accepting a proprietary JSON format with missing type conversions and upper-case attribute names. It can be suitable if you control the other side and can parse the proprietary JSON. -* **CALL TRANSFORMATION with custom XSLT**: This option is slower (~7x, depending on XSLT complexity) but allows control over the JSON format through custom XSLT, including attribute names and values. However, it works only with ABAP component names and values, without RTTI information. You need to write, deliver, and synchronize your XSLT with your data structures. - -For more details, please refer to the documentation: [JSON to ABAP transformation with the use of CALL TRANSFORMATION](https://github.com/SAP/abap-to-json/blob/main/docs/advanced.md#json-to-abap-transformation-with-the-use-of-call-transformation) - -Other kernel-based JSON parsing implementations are: -* IF_SXML_READER/IF_SXML_WRITER with IF_SXML~CO_XT_JSON (used in XCO_JSON) -* IF_JSON_READER/IF_JSON_WRITER (available from SAP_BASIS 7.57; used by [Z_UI2_JSON2](z_ui2_json2.md)) -It will parse any JSON in the kernel. But getting it mapped into ABAP data is another story. - -If you ask, why can the **CALL TRANSFORMATION id** do it fast with asJSON, but not with ANY JSON, here is the answer: - -CALL TRANSFORMATION is executed in the kernel (Core). And yes, it knows ABAP data, in some way, and can even create data (add new lines to known tables), but can not generate ABAP types (structures). And it is extremely fast, compared to implementations of IF_JSON_READER or IF_SXML_READER, because it does not have multiple callbacks from the kernel to ABAP runtime. At the same time, it is not flexible because of the simplification and normalization. ABAP is not JavaScript. ABAP is not case sensitive - you need to support different name conversion rules from JS to ABAP. It has a limit on the component name length, but JSON does not. ABAP does not have a built-in boolean (already has, but this does not help with old code) - JSON has. ABAP has conversion exits. ABAP has much more complex data type support compared to JSON (different numerics, different types of tables, date+time+timestamp), and for all these, you need bidirectional conversion. It all costs performance. If you ask for customization of the conversion (and it will be asked), it will also affect performance. And at the end, you will not be much faster than a good implementation on top of IF_JSON_READER/IF_JSON_WRITER. - -If you want to support the initiative to get full-fledged support of ANY JSON by CALL TRANSFORMATION, support [this initiative](https://influence.sap.com/sap/ino/#idea/367772). - -## GENERATE or DESERIALIZE into REF TO DATA vs. DESERIALIZE into a typed data structure -It is always better to deserialize into an explicit data structure, but not into an anonymous reference: - -1. It is faster -2. It is type-safe -3. Processing deserialized results is much easier. -Deserializing into REF TO data is the same as using the GENERATE method and results in generating real-time ABAP types, which is quite slow. You can not specify the resulting types for data elements, and the deserializer needs to guess types. To process generated results, you always use dynamic programming, which is, by default, slow (or [/UI2/CL_DATA_ACCESS](https://github.com/SAP/abap-to-json/blob/main/docs/data-access.md), which is more comfortable but still uses dynamic programming inside). - -## JSON to ABAP data type conversion when using GENERATE or DESERIALIZE into REF TO DATA -The data type selection logic of the GENERATE method (DESERIALIZE into REF TO data) is not guaranteed or defined. The class makes the best guess for the resulting ABAP data type based on the JSON value and the best-fitting data type on the ABAP side. For example, JSON booleans convert to ABAP_BOOL, JSON numbers can convert to I, P, or F types depending on the value, and JSON strings convert to date, time, or timestampl if the value matches a pattern; otherwise, they convert to a string. A new type of conversion may be introduced in the future. If you do not provide a fixed ABAP structure, you must be prepared to work with any of the generated types. Additionally, if you use the GEN_OPTIMIZE flag, you may receive direct types instead of references. For getting explicit data types, deserialize into a fixed structure. - -## Timestamp is not deserialized after PL22 -The difference that causes the changed deserialization behaviour is in the method RESTORE_TYPE. This code block: -```abap -WHEN e_typekind-ts_iso8601 OR e_typekind-tsl_iso8601. - tstml = lcl_util=>read_iso8601( sdummy ). - IF tstml IS INITIAL. - tstml = lcl_util=>read_edm_datetime( sdummy ). - IF tstml IS INITIAL. - REPLACE FIRST OCCURRENCE OF REGEX so_regex_edm_time IN sdummy WITH '$1$2$3$4$5$6.$7' REPLACEMENT LENGTH match. - IF sy-subrc EQ 0. " => Edm.Time - tstml = sdummy(match). - ENDIF. - ENDIF. - ENDIF. -``` -It was like this in PL21: -```abap -WHEN cl_abap_typedescr=>typekind_packed. - tstml = lcl_util=>read_iso8601( sdummy ). - IF tstml IS INITIAL. - tstml = lcl_util=>read_edm_datetime( sdummy ). - IF tstml IS INITIAL. - REPLACE FIRST OCCURRENCE OF REGEX so_regex_edm_time IN sdummy WITH '$1$2$3$4$5$6.$7' REPLACEMENT LENGTH match. - IF sy-subrc EQ 0. " => Edm.Time - tstml = sdummy(match). - ENDIF. - ENDIF. - ENDIF. -``` -E.g., in PL21, a recognized timestamp is written as ISO8601/Edm.DateTime/Edm.Time in a JSON string was assigned to any packed variable. This was causing errors in cases when a packed variable was not actually a real timestamp data type, but the value was still parsed as a timestamp and wrongly assigned to the packed variable (an example error in PL21, packed values with 8 digits were assigned, but with 9 not). Because of that, I have modified the logic to do a stricter check for output data type and only extract from serialized timestamps for specific data types (see method DETECT_TYPEKIND). -If, in your case, you get an initial value in the "timestamp" variable that was filled before PL22, probably your data type is not one of the known timestamp types and does not inherit from them. That is why from PL22 it is not filled anymore. Fallback logic to move a string value into packed does not work here, because the string is an ISO8601 timestamp indeed. -Supported types for now are everything that inherits (has a domain name) from TZNTSTMPS, XSDDATETIME_Z, TZNTSTMPL, XSDDATETIME_LONG_Z (see method DETECT_TYPEKIND for explicit detection logic). - -## Serialize huge data objects into JSON and short dumps -You are using the class to serialize your data into JSON. Unfortunately, sometimes you pass too big tables, which results in too long a JSON string (for example, longer than 1GB), and this leads to dumps like SYSTEM_NO_ROLL, STRING_SIZE_TOO_LARGE, and MEMORY_NO_MORE_PAGING, while the system can not allocate such a big continuous memory chunk. This specific case could be solved by increasing the memory allocation limit, but you would still end up with an INT4 size limit for string length, which can not be more than 2GB. - -The string (JSON) of such size can not be created, transported, or persisted. You would need to have special handling on your side for this case. -E.g., if you want to serialize such a large amount of data, you will need to split the input into chunks and do the serialization and transport of the resulting JSON chunks in parts. - -The memory exceptions are not catchable, and you will need to do data size evaluations on your side before calling serialization. - -So, the only robust way to solve the issue will be by limiting serialized data size, which can be done only on the /ui2/cl_json consumer side. - -Even if you select another format for serialization (XML or ABAP JSON), you will stick to some limit. So, no other way. - -If you still need to serialize everything, you may split the data into chunks and give them to the serializer one by one. Then, deserialize all fragments into the same data object for merging. - -## Encoding of Unicode characters (for example, Chinese) -The serializer does not do any explicit character encoding; this is done by ABAP. Normally, ABAP works with UTF-16, a 2-byte Unicode encoding that can represent any character (also Chinese). That is why you see Chinese characters in the debugger. Later on, after serializing in JSON (you may also check in the debugger JSON and see that Chinese characters are still in), you pass the JSON string further, maybe as a REST response. And there is, probably, converted into UTF8 encoding, which is a multibyte encoding, where some characters (Latin) are encoded with one byte and some (Chinese, Russian, etc.) are encoded with multiple bytes. Then the viewer of such UTF-8 text shall be able to interpret and display it properly. If you do not see characters as expected in your viewer tool, then, probably, nothing is corrupt, and the receiver will get them fine. It is just an issue of the viewer that does not recognize UTF-8, or has probably lost an encoding ID interpreted wrong. - -## Incompatible change for initial date/time fields serializing with PL16 -First of all, I would agree that this is an incompatible change, and I am asking you to excuse me for your efforts. It was an intentional change, and I was aware that someone could already rely on the current behavior and may have issues. - -The reason for this change of default was a customer complaint regarding the handling of initial date-time values, which are not 0000-00-00 or 00:00:00. In general, 0000-00-00 is an invalid date, 00:00:00 is valid, but how to understand that it is an initial but not an explicit midnight? - -Because of that, I have decided not to render initial values for date/time and give a receiver a way to understand that it is initial and has its default/initial processing. I know that it is incompatible, but I want a default behavior to be the best and most common choice, even with the cost of modification of the consumer code that relies on the old behavior :/. - -Because having custom rendering of the initial date/time is quite exotic, I have only let this for constructor calls and have not extended the serialize method, to keep the standard API simple. If I get multiple requests regarding extending SERIALIZE with these defaults, I will do it. - -My recommendation for you: -* Variant 1: Adopt your unit tests for new initial values for date-time. -* Variant 2: Use the instance method for serialization. E.g., parametrized CONSTRUCTOR + SERIALIZE_INT (in this case, you can customize behavior by parametrizing the instance constructor with desired initial values for time and date). -* Variant 2: Extend the /ui2/cl_json class or create a helper method in your class with your static customized SERIALIZE call, which already considers new defaults for the /ui2/cl_json constructor. - -## Is there a way to deserialize objects that have references to an Interface? -**Q**: I am using /ui2/cl_json to serialize an object that contains some reference attributes. These reference attributes are TYPE REF TO . Upon deserialization, the references are not getting deserialized. Is there a way to deserialize objects that have references to an Interface? - -**A**: Unfortunately, not. To deserialize an object, it shall be created. And how would you like to create an instance of the interface without knowing the class? It can not be done automatically. But you may try to [implement the deserialization logic by yourself](docs/advanced.md#jsonabap-serializationdeserialization-with-runtime-type-information). - -## Is it possible to have a defined order of fields in ABAP structures generated when deserializing into REF TO DATA fields? Is it possible to have the fields in the generated structure in the same order as in the JSON file? -The order of fields in JSON and also in ABAP is undefined. It may happen that you will have two records of the same type in an array, but with attributes serialized in different orders. -What to do in this case? In general, the answer is – no (there is no way to configure it). The current alphabetical order gives at least some predefined output (but the result is a name normalization and uniqueness check). - -If you want a specific order, just deserialize it in a predefined structure, but not in REF TO DATA. Generating into REF TO DATA is always a bad choice (from a performance and type definition perspective). - -But there is no easy way in the latest releases of the class. -If you still want a predefined sequence of the fields in the generated structure, you may inherit the class and prefill the structure buffer (mt_struct_type) in your inherited class constructor. See method GENERATE_STRUCT for details. In this case, later deserialize/generate calls will use your structure type, but not one created with default logic. - -## Is it possible to display the currency amount (CURR fields) formatted in the JSON output based on the related currency (CUKY field)? -No, there is no built-in support for currency fields. Potentially, one can add it in a derived class, overwriting dump_int and restore_type methods, but I do not want to have it by default, because of implementation complexity and performance penalty. - -Only single-field conversion exits are supported. - -## My fields are NOT serialized as true/false, instead, serialized like 'X' or ''! E.g., how to control ABAP/JSON Boolean conversion? -JSON, as JavaScript, has a built-in Boolean type with true/false values. ABAP does not have a built-in Boolean type and uses fields of char 1 with constant values of 'X' (TRUE) and ''(space, FALSE). Different teams use different predefined types to be used as a Boolean type for them. It is a zoo. There is no way to detect the Boolean type or even be able to auto-convert them between different ABAP types. However, there are some more or fewer standard conventions for which standard types shall be used for booleans. The serializer class has the default list of standard boolean types hardcoded in constant MC_BOOL_TYPES (ABAP_BOOLEAN, ABAP_BOOL, BOOLEAN, BOOLE_D, XFELD, XSDBOOLEAN, WDY_BOOLEAN). If you use one of these types in your data, passed to the serializer, it will be automatically processed by the parser and converted from ''/'X' into false/true and vice versa. If you use any other type not in the list, there will be no auto-conversion. OK, you do not like the default (it processes too many types or two fewer), what do you do? You have the following choices: -* Do not use static methods for serialization and deserialization, but instance ones (e.g., json_obj-> serialize_int instead of /ui2/cl_json=> serialize) AND customize the behavior of the instance by constructor parameters. You can pass an alternative set of boolean types with the parameter BOOL_TYPES. The usage of instance methods is also faster if you repeat calls for serialization. -* Inherit the class and overwrite the default boolean types, stored in the class variable mv_bool_types, with your preferred default. Use your class everywhere instead of standard, to ensure consistency. How to inherit the class, you can find [here](docs/class-extension.md). - -In addition to `BOOL_TYPES`, there is a related parameter **`BOOL_3STATE`** (default: `MC_BOOL_3STATE`). This controls which types are treated as three-state booleans (true/false/undefined, mapped to TRIBOOL values `'X'`/`'-'`/`''`). If you use a custom TRIBOOL-like type, add it to `BOOL_3STATE` the same way you would add a type to `BOOL_TYPES`. - -## I can not use /UI2/CL_JSON for ABAP Cloud BADi development -The class has been released for ABAP Cloud development (Steampunk) for a long time. Initially, it was released only for the Public Cloud, and the Private Cloud was missed by mistake. That was corrected, and now you can use it for Private Cloud from release OP 2023 (SAP_BASIS 758). See details [here](docs/history.md#note-3424850-ui2cl_json-release-api-for-cloud-development). If you need JSON processing in ABAP Cloud BADis, you may need to use the XCO library (XCO_JSON), which is meant to be the official JSON processing library for Key User Extensibility Apps. If you still think that the use of /UI2/CL_JSON would be preferable, you may ask for its release via the Customer Influence program, as it was [done for Steampunk sometime](https://influence.sap.com/sap/ino/#/idea/234724/?section=sectionVotes). - -## You get a short dump OBJECTS_NOT_CHAR when serializing data with enabled conversion exits -You can apply conversion exits to serialized data when using the /ui2/cl_json. The class uses a temporary buffer of type STRING as an output for conversion exits. But some old conversion exits support only writing in char-like variables (C LENGTH...) (restriction of WRITE TO) and dumping when WRITE TO is executed with STRING output. But the current [programming guidelines for conversion exits](https://help.sap.com/doc/abapdocu_latest_index_htm/latest/en-US/index.htm?file=abenconversion_exits.htm) say that output can be [C-LIKE type](https://help.sap.com/doc/abapdocu_latest_index_htm/latest/en-US/index.htm?file=abenbuilt_in_types_generic.htm) (c, n, and string, as well as the date/time types d, t, and character-like flat structures). So it can be a string. -If you get such a dump, please raise a message and ask the conversion exit owner to update the code to support STRING type, following the example implementation for CONVERSION_EXIT_SDURA_OUTPUT from SAP Help]: -```abap -FUNCTION CONVERSION_EXIT_SDURA_OUTPUT. -*"---------------------------------------------------------------------- -*"*"Local Interface: -*" IMPORTING -*" VALUE(INPUT) -*" EXPORTING -*" VALUE(OUTPUT) TYPE CLIKE -*"---------------------------------------------------------------------- - - hours = input DIV 60. - minutes_n = input MOD 60. - - DESCRIBE FIELD output TYPE DATA(typ). - - IF typ = 'g'. "OUTPUT is type string, no WRITE TO for strings, enabled with string templates 20130423, KELLERH - output = |{ hours WIDTH = 3 ALIGN = RIGHT }:{ minutes_n WIDTH = 2 }|. - ELSE. "Old overflow behavior to stay compatible - WRITE hours TO output(3) NO-SIGN. - output+3(1) = ':'. - WRITE minutes_n TO output+4(2). - ENDIF. -ENDFUNCTION. -``` -Use of output buffer TYPE C LENGHT ... in code of /ui2/cl_json would require an additional CONDENSE call that would negatively impact the performance of serialization and may still lead to incorrect data rendering (the logic with TYPE C LENGHT... was in PL19, but is reverted with PL 20, because of [this issue](issues/10)). - -## Why are special characters in JSON attribute names not escaped or unescaped? -This is a known limitation. Escaping, and especially unescaping, is very performance-critical and will significantly influence parsing time. Cases where attribute names contain special characters are unique — ABAP field names do not allow special characters. So, to optimize overall performance, I have decided not to support this. The only cases when the parser does escaping and unescaping the attribute names are usages of the ASSOC_ARRAYS flag when table key values are converted into leading attribute names (associative arrays in terms of JSON) and generation of the structures (internally it also uses ASSOC_ARRAY flag). - -## How to define receiving structures for my JSON? -This [online tool](https://www.findocs.xyz/tools/sap/json-to-abap) may help you to generate proper receiving ABAP structures for your input JSON. - -## How do I know if DESERIALIZE succeeded? -By default, `DESERIALIZE` never raises an exception — it silently ignores fields that don't match and returns a partial (or empty) result. This is intentional for robustness, but can be surprising. - -A common workaround is to check `IS INITIAL` after the call: -```abap -/ui2/cl_json=>deserialize( EXPORTING json = lv_json CHANGING data = ls_result ). -IF ls_result IS INITIAL. - " treat as failure -ENDIF. -``` -This is a proxy only — it will miss cases where a valid response is genuinely initial, and will pass even if only some fields were populated. - -The recommended approach is to use the instance method `DESERIALIZE_INT` with `STRICT_MODE = abap_true` in the constructor. `DESERIALIZE_INT` raises `CX_SY_MOVE_CAST_ERROR` on structural mismatch, giving you a reliable failure signal. See [Exception Handling](advanced.md#exception-handling-in-ui2cl_json). - -## How do I access fields in data returned by GENERATE? -`GENERATE` returns a `REF TO DATA` pointing to a dynamically created structure. The recommended way to navigate it is the `Z_UI2_DATA_ACCESS` (or `/UI2/CL_DATA_ACCESS`) helper class, which accepts a path expression: -```abap -DATA: lr_data TYPE REF TO data, - lv_val TYPE string. - -lr_data = /ui2/cl_json=>generate( json = `{"name":"Key1","properties":{"field1":"Value1"}}` ). - -/ui2/cl_data_access=>create( ir_data = lr_data iv_component = `properties-field1` )->value( - IMPORTING ev_data = lv_val ). -WRITE: lv_val. " -> Value1 -``` -See the full API in [data-access.md](data-access.md). - -Alternatively, without the helper class, you can chain `ASSIGN COMPONENT` calls manually — see the verbose example in [advanced.md](advanced.md#simple-generate-example). - -## Data is lost or corrupted after a DESERIALIZE / SERIALIZE round-trip - -This usually means the JSON input contains **conflicting key names** that all normalize to the same ABAP component name, or keys that cannot be represented as ABAP field names at all. - -### Conflicting keys - -The class normalizes JSON key names to ABAP component names before matching them to fields. Under the default mode (`pretty_name = none`), all keys are uppercased. Under camelCase mode, they are split on case boundaries and then uppercased. The normalization is lossy: the following three JSON keys all map to the same ABAP component `SAP_BASIS_PG_UI_MYHOME`: - -- `sapBasisPgUiMyhome` (camelCase) -- `SAP_BASIS_PG_UI_MYHOME` (already uppercase) -- `sapbasispguimyhome` (plain lowercase) - -When multiple keys map to the same component, the class applies "last value wins" — earlier values are silently overwritten. This is documented behavior. - -**Root cause**: the input JSON is inconsistent — it was accumulated from multiple sources or serialized with different `pretty_name` settings at different times. Any single consistent deserialization mode will cause collisions. - -**Solution**: use `name_mappings` to assign each distinct JSON key to a separate ABAP component name, and use the same mappings for both serialize and deserialize: - -```abap -" Define mappings once — same parameters used in both directions -DATA(lt_mappings) = VALUE /ui2/cl_json=>name_mappings( - ( abap = 'HOME_CAMEL' json = 'sapBasisPgUiMyhome' ) - ( abap = 'HOME_UPPER' json = 'SAP_BASIS_PG_UI_MYHOME' ) - ( abap = 'HOME_LOWER' json = 'sapbasispguimyhome' ) - ( abap = 'X_VERSION' json = '_version' ) -). - -" Deserialize -/ui2/cl_json=>deserialize( - EXPORTING json = lv_json - name_mappings = lt_mappings - CHANGING data = ls_data ). - -" Serialize — pass same mappings so keys are written back correctly -DATA(lv_json_out) = /ui2/cl_json=>serialize( - data = ls_data - name_mappings = lt_mappings ). -``` - -Your ABAP structure must have separate components for each mapped field (`HOME_CAMEL`, `HOME_UPPER`, `HOME_LOWER`, `X_VERSION`). - -### Keys starting with `_` or containing special characters - -ABAP component names cannot start with `_` or contain characters like `.` or `-`. Keys such as `_version` or `sap.flp` are silently dropped during deserialization unless you provide an explicit `name_mappings` entry for them (as shown above for `_version`). - -## Why does deserialization silently return empty results for my ABAP object? -Two common causes: - -**1. Mandatory constructor parameters**: If your class constructor has required importing parameters, the deserializer cannot instantiate the class and returns an unbound reference without raising an exception. Solution: add a no-argument constructor path, or initialize the target object yourself before calling `DESERIALIZE` — if the reference is already bound and of the correct type, the deserializer fills it in place. - -**2. Private/protected attributes without FRIENDS**: If the attributes you expect to be filled are not public, the deserializer cannot access them. Declare the serializer class as a `FRIEND` of your class. See [Serializing protected and private attributes](advanced.md#serializing-of-protected-and-private-attributes). - -If neither applies, enable `STRICT_MODE` and use `DESERIALIZE_INT` to surface the actual error. See [Exception Handling](advanced.md#exception-handling-in-ui2cl_json). - -# Continue reading -* [Basic usage of the class](basic.md) -* [Advanced Use cases](advanced.md) -* [Version History](history.md) +# FAQ + +* [It is slow](#it-is-slow) +* [GENERATE or DESERIALIZE into REF TO DATA vs. DESERIALIZE into a typed data structure](#generate-or-deserialize-into-ref-to-data-vs-deserialize-into-a-typed-data-structure) +* [JSON to ABAP data type conversion when using GENERATE or DESERIALIZE into REF TO DATA](#json-to-abap-data-type-conversion-when-using-generate-or-deserialize-into-ref-to-data) +* [Timestamp is not deserialized after PL22](https://github.com/SAP/abap-to-json/blob/main/docs/faq.md#timestamp-is-not-deserialized-after-pl22) +* [Serialize huge data objects into JSON and short dumps](#serialize-huge-data-objects-into-json-and-short-dumps) +* [Encoding of Unicode characters (for example, Chinese)](#encoding-of-unicode-characters-for-example-chinese) +* [Incompatible change for initial date/time fields serializing with PL16](#incompatible-change-for-initial-datetime-fields-serializing-with-pl16) +* [Is there a way to deserialize objects that have references to an Interface?](#is-there-a-way-to-deserialize-objects-that-have-references-to-an-interface) +* [Is it possible to have a defined order of fields in ABAP structures generated when deserializing into REF TO DATA fields? Is it possible to have the fields in the generated structure in the same order as in the JSON file?](#is-it-possible-to-have-a-defined-order-of-fields-in-abap-structures-generated-when-deserializing-into-ref-to-data-fields-is-it-possible-to-have-the-fields-in-the-generated-structure-in-the-same-order-as-in-the-json-file) +* [Is it possible to display the currency amount (CURR fields) formatted in the JSON output based on the related currency (CUKY field)?](#is-it-possible-to-display-the-currency-amount-curr-fields-formatted-in-the-json-output-based-on-the-related-currency-cuky-field) +* [My fields are NOT serialized as true/false, instead, serialized like 'X' or ''! E.g., how to control ABAP/JSON Boolean conversion?](#my-fields-are-not-serialized-as-truefalse-instead-serialized-like-x-or--eg-how-to-control-abapjson-boolean-conversion) +* [I can not use /UI2/CL_JSON for ABAP Cloud BADi development](#i-can-not-use-ui2cl_json-for-abap-cloud-badi-development) +* [Why does GENERATE silently drop some JSON keys — umlauts, spaces, control characters?](#why-does-generate-silently-drop-some-json-keys--umlauts-spaces-control-characters) +* [You get a short dump OBJECTS_NOT_CHAR when serializing data with enabled conversion exits](#you-get-a-short-dump-objects_not_char-when-serializing-data-with-enabled-conversion-exits) +* [Why are special characters in JSON attribute names not escaped or unescaped?](#why-are-special-characters-in-json-attribute-names-not-escaped-or-unescaped) +* [How to define receiving structures for my JSON?](#how-to-define-receiving-structures-for-my-json) +* [How do I know if DESERIALIZE succeeded?](#how-do-i-know-if-deserialize-succeeded) +* [How do I access fields in data returned by GENERATE?](#how-do-i-access-fields-in-data-returned-by-generate) +* [Why does deserialization silently return empty results for my ABAP object?](#why-does-deserialization-silently-return-empty-results-for-my-abap-object) +* [Data is lost or corrupted after a DESERIALIZE / SERIALIZE round-trip](#data-is-lost-or-corrupted-after-a-deserialize--serialize-round-trip) + +## It is slow +It is as fast as possible to achieve, and it is already heavily optimized in pure ABAP. If you have suggestions on how to make it faster, we welcome them. Features like type conversions, type detections, renaming, data generation, etc, require processing time, and even if they are not active, you may pay the penalty because the class design allows this feature. Operations on strings are not fast in ABAP, and method calls are costly, which is why macros are used within the class. However, the class is robust and can handle any data type for serialization and deserialization, offering many convenient functions that would otherwise need to be implemented manually. It performs well in numerous use cases. + +In scenarios where extensive functionality and flexibility are unnecessary, such as with simple, flat tables requiring fast JSON serialization/deserialization, tweaking /ui2/cl_json for speed by disabling flags is not viable. No other universal classes with comparable functionality are known to be faster. The best solutions currently are: + +* **CALL TRANSFORMATION id**: This is the fastest alternative (10x), but it requires accepting a proprietary JSON format with missing type conversions and upper-case attribute names. It can be suitable if you control the other side and can parse the proprietary JSON. +* **CALL TRANSFORMATION with custom XSLT**: This option is slower (~7x, depending on XSLT complexity) but allows control over the JSON format through custom XSLT, including attribute names and values. However, it works only with ABAP component names and values, without RTTI information. You need to write, deliver, and synchronize your XSLT with your data structures. + +For more details, please refer to the documentation: [JSON to ABAP transformation with the use of CALL TRANSFORMATION](https://github.com/SAP/abap-to-json/blob/main/docs/advanced.md#json-to-abap-transformation-with-the-use-of-call-transformation) + +Other kernel-based JSON parsing implementations are: +* IF_SXML_READER/IF_SXML_WRITER with IF_SXML~CO_XT_JSON (used in XCO_JSON) +* IF_JSON_READER/IF_JSON_WRITER (available from SAP_BASIS 7.57; used by [Z_UI2_JSON2](z_ui2_json2.md)) +It will parse any JSON in the kernel. But getting it mapped into ABAP data is another story. + +If you ask, why can the **CALL TRANSFORMATION id** do it fast with asJSON, but not with ANY JSON, here is the answer: + +CALL TRANSFORMATION is executed in the kernel (Core). And yes, it knows ABAP data, in some way, and can even create data (add new lines to known tables), but can not generate ABAP types (structures). And it is extremely fast, compared to implementations of IF_JSON_READER or IF_SXML_READER, because it does not have multiple callbacks from the kernel to ABAP runtime. At the same time, it is not flexible because of the simplification and normalization. ABAP is not JavaScript. ABAP is not case sensitive - you need to support different name conversion rules from JS to ABAP. It has a limit on the component name length, but JSON does not. ABAP does not have a built-in boolean (already has, but this does not help with old code) - JSON has. ABAP has conversion exits. ABAP has much more complex data type support compared to JSON (different numerics, different types of tables, date+time+timestamp), and for all these, you need bidirectional conversion. It all costs performance. If you ask for customization of the conversion (and it will be asked), it will also affect performance. And at the end, you will not be much faster than a good implementation on top of IF_JSON_READER/IF_JSON_WRITER. + +If you want to support the initiative to get full-fledged support of ANY JSON by CALL TRANSFORMATION, support [this initiative](https://influence.sap.com/sap/ino/#idea/367772). + +## GENERATE or DESERIALIZE into REF TO DATA vs. DESERIALIZE into a typed data structure +It is always better to deserialize into an explicit data structure, but not into an anonymous reference: + +1. It is faster +2. It is type-safe +3. Processing deserialized results is much easier. +Deserializing into REF TO data is the same as using the GENERATE method and results in generating real-time ABAP types, which is quite slow. You can not specify the resulting types for data elements, and the deserializer needs to guess types. To process generated results, you always use dynamic programming, which is, by default, slow (or [/UI2/CL_DATA_ACCESS](https://github.com/SAP/abap-to-json/blob/main/docs/data-access.md), which is more comfortable but still uses dynamic programming inside). + +## JSON to ABAP data type conversion when using GENERATE or DESERIALIZE into REF TO DATA +The data type selection logic of the GENERATE method (DESERIALIZE into REF TO data) is not guaranteed or defined. The class makes the best guess for the resulting ABAP data type based on the JSON value and the best-fitting data type on the ABAP side. For example, JSON booleans convert to ABAP_BOOL, JSON numbers can convert to I, P, or F types depending on the value, and JSON strings convert to date, time, or timestampl if the value matches a pattern; otherwise, they convert to a string. A new type of conversion may be introduced in the future. If you do not provide a fixed ABAP structure, you must be prepared to work with any of the generated types. Additionally, if you use the GEN_OPTIMIZE flag, you may receive direct types instead of references. For getting explicit data types, deserialize into a fixed structure. + +## Timestamp is not deserialized after PL22 +The difference that causes the changed deserialization behaviour is in the method RESTORE_TYPE. This code block: +```abap +WHEN e_typekind-ts_iso8601 OR e_typekind-tsl_iso8601. + tstml = lcl_util=>read_iso8601( sdummy ). + IF tstml IS INITIAL. + tstml = lcl_util=>read_edm_datetime( sdummy ). + IF tstml IS INITIAL. + REPLACE FIRST OCCURRENCE OF REGEX so_regex_edm_time IN sdummy WITH '$1$2$3$4$5$6.$7' REPLACEMENT LENGTH match. + IF sy-subrc EQ 0. " => Edm.Time + tstml = sdummy(match). + ENDIF. + ENDIF. + ENDIF. +``` +It was like this in PL21: +```abap +WHEN cl_abap_typedescr=>typekind_packed. + tstml = lcl_util=>read_iso8601( sdummy ). + IF tstml IS INITIAL. + tstml = lcl_util=>read_edm_datetime( sdummy ). + IF tstml IS INITIAL. + REPLACE FIRST OCCURRENCE OF REGEX so_regex_edm_time IN sdummy WITH '$1$2$3$4$5$6.$7' REPLACEMENT LENGTH match. + IF sy-subrc EQ 0. " => Edm.Time + tstml = sdummy(match). + ENDIF. + ENDIF. + ENDIF. +``` +E.g., in PL21, a recognized timestamp is written as ISO8601/Edm.DateTime/Edm.Time in a JSON string was assigned to any packed variable. This was causing errors in cases when a packed variable was not actually a real timestamp data type, but the value was still parsed as a timestamp and wrongly assigned to the packed variable (an example error in PL21, packed values with 8 digits were assigned, but with 9 not). Because of that, I have modified the logic to do a stricter check for output data type and only extract from serialized timestamps for specific data types (see method DETECT_TYPEKIND). +If, in your case, you get an initial value in the "timestamp" variable that was filled before PL22, probably your data type is not one of the known timestamp types and does not inherit from them. That is why from PL22 it is not filled anymore. Fallback logic to move a string value into packed does not work here, because the string is an ISO8601 timestamp indeed. +Supported types for now are everything that inherits (has a domain name) from TZNTSTMPS, XSDDATETIME_Z, TZNTSTMPL, XSDDATETIME_LONG_Z (see method DETECT_TYPEKIND for explicit detection logic). + +## Serialize huge data objects into JSON and short dumps +You are using the class to serialize your data into JSON. Unfortunately, sometimes you pass too big tables, which results in too long a JSON string (for example, longer than 1GB), and this leads to dumps like SYSTEM_NO_ROLL, STRING_SIZE_TOO_LARGE, and MEMORY_NO_MORE_PAGING, while the system can not allocate such a big continuous memory chunk. This specific case could be solved by increasing the memory allocation limit, but you would still end up with an INT4 size limit for string length, which can not be more than 2GB. + +The string (JSON) of such size can not be created, transported, or persisted. You would need to have special handling on your side for this case. +E.g., if you want to serialize such a large amount of data, you will need to split the input into chunks and do the serialization and transport of the resulting JSON chunks in parts. + +The memory exceptions are not catchable, and you will need to do data size evaluations on your side before calling serialization. + +So, the only robust way to solve the issue will be by limiting serialized data size, which can be done only on the /ui2/cl_json consumer side. + +Even if you select another format for serialization (XML or ABAP JSON), you will stick to some limit. So, no other way. + +If you still need to serialize everything, you may split the data into chunks and give them to the serializer one by one. Then, deserialize all fragments into the same data object for merging. + +## Encoding of Unicode characters (for example, Chinese) +The serializer does not do any explicit character encoding; this is done by ABAP. Normally, ABAP works with UTF-16, a 2-byte Unicode encoding that can represent any character (also Chinese). That is why you see Chinese characters in the debugger. Later on, after serializing in JSON (you may also check in the debugger JSON and see that Chinese characters are still in), you pass the JSON string further, maybe as a REST response. And there is, probably, converted into UTF8 encoding, which is a multibyte encoding, where some characters (Latin) are encoded with one byte and some (Chinese, Russian, etc.) are encoded with multiple bytes. Then the viewer of such UTF-8 text shall be able to interpret and display it properly. If you do not see characters as expected in your viewer tool, then, probably, nothing is corrupt, and the receiver will get them fine. It is just an issue of the viewer that does not recognize UTF-8, or has probably lost an encoding ID interpreted wrong. + +## Incompatible change for initial date/time fields serializing with PL16 +First of all, I would agree that this is an incompatible change, and I am asking you to excuse me for your efforts. It was an intentional change, and I was aware that someone could already rely on the current behavior and may have issues. + +The reason for this change of default was a customer complaint regarding the handling of initial date-time values, which are not 0000-00-00 or 00:00:00. In general, 0000-00-00 is an invalid date, 00:00:00 is valid, but how to understand that it is an initial but not an explicit midnight? + +Because of that, I have decided not to render initial values for date/time and give a receiver a way to understand that it is initial and has its default/initial processing. I know that it is incompatible, but I want a default behavior to be the best and most common choice, even with the cost of modification of the consumer code that relies on the old behavior :/. + +Because having custom rendering of the initial date/time is quite exotic, I have only let this for constructor calls and have not extended the serialize method, to keep the standard API simple. If I get multiple requests regarding extending SERIALIZE with these defaults, I will do it. + +My recommendation for you: +* Variant 1: Adopt your unit tests for new initial values for date-time. +* Variant 2: Use the instance method for serialization. E.g., parametrized CONSTRUCTOR + SERIALIZE_INT (in this case, you can customize behavior by parametrizing the instance constructor with desired initial values for time and date). +* Variant 2: Extend the /ui2/cl_json class or create a helper method in your class with your static customized SERIALIZE call, which already considers new defaults for the /ui2/cl_json constructor. + +## Is there a way to deserialize objects that have references to an Interface? +**Q**: I am using /ui2/cl_json to serialize an object that contains some reference attributes. These reference attributes are TYPE REF TO . Upon deserialization, the references are not getting deserialized. Is there a way to deserialize objects that have references to an Interface? + +**A**: Unfortunately, not. To deserialize an object, it shall be created. And how would you like to create an instance of the interface without knowing the class? It can not be done automatically. But you may try to [implement the deserialization logic by yourself](docs/advanced.md#jsonabap-serializationdeserialization-with-runtime-type-information). + +## Is it possible to have a defined order of fields in ABAP structures generated when deserializing into REF TO DATA fields? Is it possible to have the fields in the generated structure in the same order as in the JSON file? +The order of fields in JSON and also in ABAP is undefined. It may happen that you will have two records of the same type in an array, but with attributes serialized in different orders. +What to do in this case? In general, the answer is – no (there is no way to configure it). The current alphabetical order gives at least some predefined output (but the result is a name normalization and uniqueness check). + +If you want a specific order, just deserialize it in a predefined structure, but not in REF TO DATA. Generating into REF TO DATA is always a bad choice (from a performance and type definition perspective). + +But there is no easy way in the latest releases of the class. +If you still want a predefined sequence of the fields in the generated structure, you may inherit the class and prefill the structure buffer (mt_struct_type) in your inherited class constructor. See method GENERATE_STRUCT for details. In this case, later deserialize/generate calls will use your structure type, but not one created with default logic. + +## Is it possible to display the currency amount (CURR fields) formatted in the JSON output based on the related currency (CUKY field)? +No, there is no built-in support for currency fields. Potentially, one can add it in a derived class, overwriting dump_int and restore_type methods, but I do not want to have it by default, because of implementation complexity and performance penalty. + +Only single-field conversion exits are supported. + +## My fields are NOT serialized as true/false, instead, serialized like 'X' or ''! E.g., how to control ABAP/JSON Boolean conversion? +JSON, as JavaScript, has a built-in Boolean type with true/false values. ABAP does not have a built-in Boolean type and uses fields of char 1 with constant values of 'X' (TRUE) and ''(space, FALSE). Different teams use different predefined types to be used as a Boolean type for them. It is a zoo. There is no way to detect the Boolean type or even be able to auto-convert them between different ABAP types. However, there are some more or fewer standard conventions for which standard types shall be used for booleans. The serializer class has the default list of standard boolean types hardcoded in constant MC_BOOL_TYPES (ABAP_BOOLEAN, ABAP_BOOL, BOOLEAN, BOOLE_D, XFELD, XSDBOOLEAN, WDY_BOOLEAN). If you use one of these types in your data, passed to the serializer, it will be automatically processed by the parser and converted from ''/'X' into false/true and vice versa. If you use any other type not in the list, there will be no auto-conversion. OK, you do not like the default (it processes too many types or two fewer), what do you do? You have the following choices: +* Do not use static methods for serialization and deserialization, but instance ones (e.g., json_obj-> serialize_int instead of /ui2/cl_json=> serialize) AND customize the behavior of the instance by constructor parameters. You can pass an alternative set of boolean types with the parameter BOOL_TYPES. The usage of instance methods is also faster if you repeat calls for serialization. +* Inherit the class and overwrite the default boolean types, stored in the class variable mv_bool_types, with your preferred default. Use your class everywhere instead of standard, to ensure consistency. How to inherit the class, you can find [here](docs/class-extension.md). + +In addition to `BOOL_TYPES`, there is a related parameter **`BOOL_3STATE`** (default: `MC_BOOL_3STATE`). This controls which types are treated as three-state booleans (true/false/undefined, mapped to TRIBOOL values `'X'`/`'-'`/`''`). If you use a custom TRIBOOL-like type, add it to `BOOL_3STATE` the same way you would add a type to `BOOL_TYPES`. + +## I can not use /UI2/CL_JSON for ABAP Cloud BADi development +The class has been released for ABAP Cloud development (Steampunk) for a long time. Initially, it was released only for the Public Cloud, and the Private Cloud was missed by mistake. That was corrected, and now you can use it for Private Cloud from release OP 2023 (SAP_BASIS 758). See details [here](docs/history.md#note-3424850-ui2cl_json-release-api-for-cloud-development). If you need JSON processing in ABAP Cloud BADis, you may need to use the XCO library (XCO_JSON), which is meant to be the official JSON processing library for Key User Extensibility Apps. If you still think that the use of /UI2/CL_JSON would be preferable, you may ask for its release via the Customer Influence program, as it was [done for Steampunk sometime](https://influence.sap.com/sap/ino/#/idea/234724/?section=sectionVotes). + +## You get a short dump OBJECTS_NOT_CHAR when serializing data with enabled conversion exits +You can apply conversion exits to serialized data when using the /ui2/cl_json. The class uses a temporary buffer of type STRING as an output for conversion exits. But some old conversion exits support only writing in char-like variables (C LENGTH...) (restriction of WRITE TO) and dumping when WRITE TO is executed with STRING output. But the current [programming guidelines for conversion exits](https://help.sap.com/doc/abapdocu_latest_index_htm/latest/en-US/index.htm?file=abenconversion_exits.htm) say that output can be [C-LIKE type](https://help.sap.com/doc/abapdocu_latest_index_htm/latest/en-US/index.htm?file=abenbuilt_in_types_generic.htm) (c, n, and string, as well as the date/time types d, t, and character-like flat structures). So it can be a string. +If you get such a dump, please raise a message and ask the conversion exit owner to update the code to support STRING type, following the example implementation for CONVERSION_EXIT_SDURA_OUTPUT from SAP Help]: +```abap +FUNCTION CONVERSION_EXIT_SDURA_OUTPUT. +*"---------------------------------------------------------------------- +*"*"Local Interface: +*" IMPORTING +*" VALUE(INPUT) +*" EXPORTING +*" VALUE(OUTPUT) TYPE CLIKE +*"---------------------------------------------------------------------- + + hours = input DIV 60. + minutes_n = input MOD 60. + + DESCRIBE FIELD output TYPE DATA(typ). + + IF typ = 'g'. "OUTPUT is type string, no WRITE TO for strings, enabled with string templates 20130423, KELLERH + output = |{ hours WIDTH = 3 ALIGN = RIGHT }:{ minutes_n WIDTH = 2 }|. + ELSE. "Old overflow behavior to stay compatible + WRITE hours TO output(3) NO-SIGN. + output+3(1) = ':'. + WRITE minutes_n TO output+4(2). + ENDIF. +ENDFUNCTION. +``` +Use of output buffer TYPE C LENGHT ... in code of /ui2/cl_json would require an additional CONDENSE call that would negatively impact the performance of serialization and may still lead to incorrect data rendering (the logic with TYPE C LENGHT... was in PL19, but is reverted with PL 20, because of [this issue](issues/10)). + +## Why are special characters in JSON attribute names not escaped or unescaped? +This is a known limitation. Escaping, and especially unescaping, is very performance-critical and will significantly influence parsing time. Cases where attribute names contain special characters are unique — ABAP field names do not allow special characters. So, to optimize overall performance, I have decided not to support this. The only cases when the parser does escaping and unescaping the attribute names are usages of the ASSOC_ARRAYS flag when table key values are converted into leading attribute names (associative arrays in terms of JSON) and generation of the structures (internally it also uses ASSOC_ARRAY flag). + +## How to define receiving structures for my JSON? +This [online tool](https://www.findocs.xyz/tools/sap/json-to-abap) may help you to generate proper receiving ABAP structures for your input JSON. + +## How do I know if DESERIALIZE succeeded? +By default, `DESERIALIZE` never raises an exception — it silently ignores fields that don't match and returns a partial (or empty) result. This is intentional for robustness, but can be surprising. + +A common workaround is to check `IS INITIAL` after the call: +```abap +/ui2/cl_json=>deserialize( EXPORTING json = lv_json CHANGING data = ls_result ). +IF ls_result IS INITIAL. + " treat as failure +ENDIF. +``` +This is a proxy only — it will miss cases where a valid response is genuinely initial, and will pass even if only some fields were populated. + +The recommended approach is to use the instance method `DESERIALIZE_INT` with `STRICT_MODE = abap_true` in the constructor. `DESERIALIZE_INT` raises `CX_SY_MOVE_CAST_ERROR` on structural mismatch, giving you a reliable failure signal. See [Exception Handling](advanced.md#exception-handling-in-ui2cl_json). + +## How do I access fields in data returned by GENERATE? +`GENERATE` returns a `REF TO DATA` pointing to a dynamically created structure. The recommended way to navigate it is the `Z_UI2_DATA_ACCESS` (or `/UI2/CL_DATA_ACCESS`) helper class, which accepts a path expression: +```abap +DATA: lr_data TYPE REF TO data, + lv_val TYPE string. + +lr_data = /ui2/cl_json=>generate( json = `{"name":"Key1","properties":{"field1":"Value1"}}` ). + +/ui2/cl_data_access=>create( ir_data = lr_data iv_component = `properties-field1` )->value( + IMPORTING ev_data = lv_val ). +WRITE: lv_val. " -> Value1 +``` +See the full API in [data-access.md](data-access.md). + +Alternatively, without the helper class, you can chain `ASSIGN COMPONENT` calls manually — see the verbose example in [advanced.md](advanced.md#simple-generate-example). + +## Data is lost or corrupted after a DESERIALIZE / SERIALIZE round-trip + +This usually means the JSON input contains **conflicting key names** that all normalize to the same ABAP component name, or keys that cannot be represented as ABAP field names at all. + +### Conflicting keys + +The class normalizes JSON key names to ABAP component names before matching them to fields. Under the default mode (`pretty_name = none`), all keys are uppercased. Under camelCase mode, they are split on case boundaries and then uppercased. The normalization is lossy: the following three JSON keys all map to the same ABAP component `SAP_BASIS_PG_UI_MYHOME`: + +- `sapBasisPgUiMyhome` (camelCase) +- `SAP_BASIS_PG_UI_MYHOME` (already uppercase) +- `sapbasispguimyhome` (plain lowercase) + +When multiple keys map to the same component, the class applies "last value wins" — earlier values are silently overwritten. This is documented behavior. + +**Root cause**: the input JSON is inconsistent — it was accumulated from multiple sources or serialized with different `pretty_name` settings at different times. Any single consistent deserialization mode will cause collisions. + +**Solution**: use `name_mappings` to assign each distinct JSON key to a separate ABAP component name, and use the same mappings for both serialize and deserialize: + +```abap +" Define mappings once — same parameters used in both directions +DATA(lt_mappings) = VALUE /ui2/cl_json=>name_mappings( + ( abap = 'HOME_CAMEL' json = 'sapBasisPgUiMyhome' ) + ( abap = 'HOME_UPPER' json = 'SAP_BASIS_PG_UI_MYHOME' ) + ( abap = 'HOME_LOWER' json = 'sapbasispguimyhome' ) + ( abap = 'X_VERSION' json = '_version' ) +). + +" Deserialize +/ui2/cl_json=>deserialize( + EXPORTING json = lv_json + name_mappings = lt_mappings + CHANGING data = ls_data ). + +" Serialize — pass same mappings so keys are written back correctly +DATA(lv_json_out) = /ui2/cl_json=>serialize( + data = ls_data + name_mappings = lt_mappings ). +``` + +Your ABAP structure must have separate components for each mapped field (`HOME_CAMEL`, `HOME_UPPER`, `HOME_LOWER`, `X_VERSION`). + +### Keys starting with `_` or containing special characters + +ABAP component names cannot start with `_` or contain characters like `.` or `-`. Keys such as `_version` or `sap.flp` are silently dropped during deserialization unless you provide an explicit `name_mappings` entry for them (as shown above for `_version`). + +## Why does deserialization silently return empty results for my ABAP object? +Two common causes: + +**1. Mandatory constructor parameters**: If your class constructor has required importing parameters, the deserializer cannot instantiate the class and returns an unbound reference without raising an exception. Solution: add a no-argument constructor path, or initialize the target object yourself before calling `DESERIALIZE` — if the reference is already bound and of the correct type, the deserializer fills it in place. + +**2. Private/protected attributes without FRIENDS**: If the attributes you expect to be filled are not public, the deserializer cannot access them. Declare the serializer class as a `FRIEND` of your class. See [Serializing protected and private attributes](advanced.md#serializing-of-protected-and-private-attributes). + +If neither applies, enable `STRICT_MODE` and use `DESERIALIZE_INT` to surface the actual error. See [Exception Handling](advanced.md#exception-handling-in-ui2cl_json). + +## Why does GENERATE silently drop some JSON keys — umlauts, spaces, control characters? + +The `GENERATE` method creates ABAP data types dynamically from arbitrary JSON. To produce valid ABAP component names it applies two steps: + +**Step 1 — Normalization:** The regex `[^0-9a-zA-Z_]+` replaces any sequence of non-alphanumeric characters with a single `_`. Examples: +- `/COMP/A` → `_COMP_A` +- `TEST 2` → `TEST_2` +- `äöü` → `_` +- ` ` (space only) → `_` +- `` (null char) → `_` + +**Step 2 — Collision dedup:** Each normalized name is inserted into a unique hashed table. If a name already exists (collision), **the duplicate entry is silently dropped**. + +This means that multiple keys which all normalize to `_` (e.g. a space-only key, umlauts, control characters) will all compete for the same slot — only the first one wins, the rest are dropped. + +**Why this is the right behavior:** `GENERATE` cannot invent readable names for non-ABAP-compatible keys. Generating synthetic unique names (e.g. `__1`, `__2`) would make the resulting structure unreadable and non-round-trippable. The method does the **optimal minimum**: preserve what can be preserved, silently discard what cannot be represented. + +**If you need to preserve such keys:** Do not use `GENERATE`. Instead, deserialize into a typed ABAP structure where you control the field names, or pre-process the JSON to normalize keys before calling `GENERATE`. + +*Added 2026-07-01 based on question from Daniel Reger (daniel.reger@sap.com).* + +# Continue reading +* [Basic usage of the class](basic.md) +* [Advanced Use cases](advanced.md) +* [Version History](history.md) diff --git a/src/z_ui2_json2.clas.locals_imp.abap b/src/z_ui2_json2.clas.locals_imp.abap index 512554e..f6d7c14 100644 --- a/src/z_ui2_json2.clas.locals_imp.abap +++ b/src/z_ui2_json2.clas.locals_imp.abap @@ -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 diff --git a/src/z_ui2_json2.clas.testclasses.abap b/src/z_ui2_json2.clas.testclasses.abap index bd9733f..d3140a1 100644 --- a/src/z_ui2_json2.clas.testclasses.abap +++ b/src/z_ui2_json2.clas.testclasses.abap @@ -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 * ---------------------------------------------------------------------- @@ -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 \ No newline at end of file From c072a993a87c722d64d06ad6e909a07034633482 Mon Sep 17 00:00:00 2001 From: Alexey Arsenyev <30653201+alexey-arseniev@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:29:25 +0200 Subject: [PATCH 2/5] gitignore: exclude kb/ and *.log files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 9a29cbc..4a130ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ CLAUDE.md .claude/ +kb/ +*.log From f51a2198a64d8c179030392197027c1b394b430f Mon Sep 17 00:00:00 2001 From: Alexey Arsenyev <30653201+alexey-arseniev@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:48:06 +0200 Subject: [PATCH 3/5] fix(z_ui2_json): serialize DECFLOAT16/DECFLOAT34 as numeric JSON, not null MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-value DECFLOAT16/DECFLOAT34 fields were falling through to the final ELSE branch (serialize as string), which emitted null for initial values. Add explicit WHEN branches matching typekind_decfloat16 and typekind_decfloat34 — zero → '0', non-zero → CONDENSE'd string value. Mirrors the existing FLOAT handling pattern. Adds unit test. --- src/z_ui2_json.clas.abap | 7 +++++++ src/z_ui2_json.clas.macros.abap | 7 +++++++ src/z_ui2_json.clas.testclasses.abap | 31 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/z_ui2_json.clas.abap b/src/z_ui2_json.clas.abap index 6363e87..bc1ab2d 100644 --- a/src/z_ui2_json.clas.abap +++ b/src/z_ui2_json.clas.abap @@ -1098,6 +1098,13 @@ CLASS Z_UI2_JSON IMPLEMENTATION. ELSE. r_json = data. ENDIF. + WHEN cl_abap_typedescr=>typekind_decfloat16 OR cl_abap_typedescr=>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`. diff --git a/src/z_ui2_json.clas.macros.abap b/src/z_ui2_json.clas.macros.abap index 7832511..a6403c0 100644 --- a/src/z_ui2_json.clas.macros.abap +++ b/src/z_ui2_json.clas.macros.abap @@ -125,6 +125,13 @@ DEFINE dump_type_int. ELSE. &3 = &1. ENDIF. + WHEN cl_abap_typedescr=>typekind_decfloat16 OR cl_abap_typedescr=>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`. diff --git a/src/z_ui2_json.clas.testclasses.abap b/src/z_ui2_json.clas.testclasses.abap index dc851d8..d6ba92c 100644 --- a/src/z_ui2_json.clas.testclasses.abap +++ b/src/z_ui2_json.clas.testclasses.abap @@ -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 * ---------------------------------------------------------------------- @@ -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 From cf240e528c570060561d56bac8c5722abe9c063f Mon Sep 17 00:00:00 2001 From: Alexey Arsenyev <30653201+alexey-arseniev@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:52:08 +0200 Subject: [PATCH 4/5] fix(z_ui2_json): use local e_typekind constants for decfloat16/34 cl_abap_typedescr=>typekind_decfloat16/34 are not available on 7.31 (introduced in 7.40). Replace with local e_typekind-decfloat16/34 constants ('a'/'e') following the same pattern as utclong, int8, enum. z_ui2_json2 targets 7.57+ so it can keep the class constants directly. --- src/z_ui2_json.clas.abap | 4 +++- src/z_ui2_json.clas.macros.abap | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/z_ui2_json.clas.abap b/src/z_ui2_json.clas.abap index bc1ab2d..0f16e03 100644 --- a/src/z_ui2_json.clas.abap +++ b/src/z_ui2_json.clas.abap @@ -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, @@ -1098,7 +1100,7 @@ CLASS Z_UI2_JSON IMPLEMENTATION. ELSE. r_json = data. ENDIF. - WHEN cl_abap_typedescr=>typekind_decfloat16 OR cl_abap_typedescr=>typekind_decfloat34. + WHEN e_typekind-decfloat16 OR e_typekind-decfloat34. IF data IS INITIAL. r_json = `0`. ELSE. diff --git a/src/z_ui2_json.clas.macros.abap b/src/z_ui2_json.clas.macros.abap index a6403c0..4f6da26 100644 --- a/src/z_ui2_json.clas.macros.abap +++ b/src/z_ui2_json.clas.macros.abap @@ -125,7 +125,7 @@ DEFINE dump_type_int. ELSE. &3 = &1. ENDIF. - WHEN cl_abap_typedescr=>typekind_decfloat16 OR cl_abap_typedescr=>typekind_decfloat34. + WHEN z_ui2_json=>e_typekind-decfloat16 OR z_ui2_json=>e_typekind-decfloat34. IF &1 IS INITIAL. &3 = `0`. ELSE. From 1e38414086c8b93d1eb9904224654a827a9e031d Mon Sep 17 00:00:00 2001 From: Alexey Arsenyev <30653201+alexey-arseniev@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:03:13 +0200 Subject: [PATCH 5/5] docs: add implementation evaluation for path-based deserialization (1.2) Expand feature 1.2 with an implementation evaluation targeting the next patch level across all three classes (Z_UI2_JSON, /UI2/CL_JSON, Z_UI2_JSON2): API shape, hook points in each parser, reuse of existing skip/consume primitives, and performance analysis for both flag-unused (hot path stays neutral) and flag-used cases. v1 scoped to object-member traversal only. Records the performance-neutrality design rule. --- docs/feature-requests.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/feature-requests.md b/docs/feature-requests.md index ea8703c..f8b5d52 100644 --- a/docs/feature-requests.md +++ b/docs/feature-requests.md @@ -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.