mattcasters commented on issue #8711:
URL: https://github.com/apache/hop/issues/8711#issuecomment-5918849385

   ## JSON implementation plan — part 1 of 3
   
   The full saved plan exceeds one GitHub comment, so it is published in 3 
parts. This is a proposed implementation plan; code snippets and expected test 
results have not been executed. The following comments contain the remaining 
parts.
   
   ---
   
   <!--
   SPDX-License-Identifier: Apache-2.0
   
   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at
   
       https://www.apache.org/licenses/LICENSE-2.0
   
   Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
   -->
   
   # Apache Hop #8711: phased JSON implementation plan
   
   ## Goal
   
   Resolve or explicitly disposition every child of 
[apache/hop#8711](https://github.com/apache/hop/issues/8711), improving JSON 
correctness and bounded-memory file output without silently changing existing 
pipelines or duplicating work already in review.
   
   ## Current context / assumptions
   
   ### Evidence and scope
   
   - Inspected workspace: `/home/matt/git/mattcasters/hop`, branch 
`issue-8711`, clean tracked worktree at 
`2fbe81f7caeb92745be63396f0d6b17b89dd6137` before writing this plan.
   - Issue bodies and **all returned comments** for the umbrella and its 15 
children were read on 2026-09-30. Collection was checked programmatically: 15 
entries, 15 unique issue numbers.
   - This is a **planning-only** deliverable. No Maven builds, tests, checkout, 
commits, issue edits, or merges were executed. Maven writes `target/` and 
invokes Spotless, so even a baseline test run would violate this turn's 
restrictions. Every RED/GREEN result below is a required future outcome, **not 
an observed result**. Do not quote prospective test counts as measurements.
   - Live JDK is OpenJDK 21.0.12.1. Root `pom.xml` targets Java 21. The JSON 
module is `plugins/transforms/json/pom.xml`, artifact `hop-transform-json`, 
version `2.20.0-SNAPSHOT`.
   - Jackson databind/core are pinned to `2.21.5` in `lib/pom.xml`; Jayway 
JsonPath is `2.10.0` in root `pom.xml`. Local jars were checked with `javap`: 
`ObjectMapper.readTree/readValue/writeValueAsString/convertValue/valueToTree`, 
`JsonFactory.createGenerator(Writer)`, 
`JsonGenerator.writeTree/writeStartArray/writeEndArray`, and 
`JacksonJsonNodeJsonProvider(ObjectMapper)` exist.
   - **json-simple and json-smart are different libraries.** #7995 concerns 
`org.json.simple` / `com.googlecode.json-simple:json-simple:1.1.1`. Jayway's 
current string-input provider uses `net.minidev:json-smart:2.6.0`; do not 
remove or replace that provider as a side effect of #7995.
   - Repository rules: read `AGENTS.md` and `CONTRIBUTING.md`; retain persisted 
XML keys and transform IDs; use Hop VFS for file I/O; use Lombok; use 
annotated, grouped widgets for new settings. Preserve existing custom tables 
and existing forms rather than rebuilding every dialog.
   
   ### A real source prerequisite, not an assumed green baseline
   
   
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java:319`
 literally contains the invalid Java identifier `getKeyJson...Name`. The same 
token occurs at its call sites. Both a filesystem read and `git show 
HEAD:<path>` returned it, despite the clean worktree. PR #8713's diff also 
retains that token. This cannot be treated as a valid accessor or silently 
normalized in the plan. Task 0.2 explicitly diagnoses and, if it remains 
present on the implementation baseline, repairs this syntax prerequisite before 
feature tests. GitHub reports passing checks on the PR snapshots; that does 
**not** establish that this local source compiles.
   
   ### Child issue disposition and effort
   
   All rows remain tracker obligations until verification or an explicit 
maintainer decision. “Already implemented” means source evidence, not that the 
ticket has been closed.
   
   | Child | Requirement | Evidence / proposed disposition | Effort | Phase |
   |---|---|---|---|---|
   | [#2569](https://github.com/apache/hop/issues/2569) | Enhanced output 
differs from old wiki example | Reproduce the attached pipeline; document 
grouping versus field serialization, do not change output solely to match an 
obsolete screenshot | Low | 1 / 4 |
   | [#2570](https://github.com/apache/hop/issues/2570) | Missing/renamed group 
key produces unexpected exception | Validate key lookup **before** adding its 
metadata, in both metadata and runtime paths; include the missing name | Low | 
2 |
   | [#2572](https://github.com/apache/hop/issues/2572) | Compatibility tooltip 
claims Hop 4.3.0 | English tooltip is already behavior-based; stale Italian 
legacy key remains. Correct that translation; no new compatibility mode | Low | 
1 |
   | [#2929](https://github.com/apache/hop/issues/2929) | Streaming enhanced 
output | **Reuse/review open PR #8713**, not a second implementation. Its 
row-at-a-time file mode does not prove constant memory for one enormous group | 
High; existing implementation | 4 |
   | [#4344](https://github.com/apache/hop/issues/4344) | NDJSON append | Add 
an opt-in, GUI-backed format after streaming has landed. Preserve the default 
concatenated-document append behavior | Medium | 5 |
   | [#4373](https://github.com/apache/hop/issues/4373) | Distinguish JSON null 
from empty string with system setting | Fix JSON converter's null input 
handoff; do not alter global CSV/text null conventions | Low | 2 |
   | [#4395](https://github.com/apache/hop/issues/4395) | Object key name/value 
by ordinal | Recommend declining custom `$.[0].name()` syntax: objects do not 
have portable positional meaning. Document quoted-key lookup, existing 
`keys()`, and an explicit ordered entries array | Low decision/docs | 1 |
   | [#4644](https://github.com/apache/hop/issues/4644) | JSON-typed source 
fields | Source already routes `TYPE_JSON` through `jsonFieldIterator()` and 
Jackson provider; existing `0007-json-input-json-value` integration test 
exists. Verify and document rather than adding generic Object/Map coercion | 
Low verification/docs | 1 |
   | [#4907](https://github.com/apache/hop/issues/4907) | Timestamp conversion, 
NPE, UI freeze | Current parser has both null guards; source/tests support >3 
`S` fraction digits. Reporter mask requires an offset absent from the string. 
Verify corrected mask and invalid-mask failure; treat freeze separately if it 
reproduces | Low verification, possible separate bug | 1 |
   | [#5167](https://github.com/apache/hop/issues/5167) | Group Key Get Fields 
| Add button that inserts upstream fields not already mapped on Fields or Group 
Key, preserving mappings and order | Medium | 3 |
   | [#5313](https://github.com/apache/hop/issues/5313) | Grouping 
documentation / element names | Shares #5167; element name **is** used in file 
group envelopes, but does not rename forwarded row keys. Clarify instead of 
deleting it | Low, dependent on 3 / 4 | 3 / 4 |
   | [#5953](https://github.com/apache/hop/issues/5953) | Configured source 
field disappears on upstream lookup failure | **Reuse/review open PR #8588**, 
including its common UI helpers and tests; do not duplicate its broad dialog 
sweep | Medium review; existing implementation | 0 / 3 |
   | [#7995](https://github.com/apache/hop/issues/7995) | Remove json-simple | 
Cross-repository migration, not just JSON transforms; 68 Java source/test 
files. Public signatures expose JSONObject. Approve compatibility policy, then 
migrate callers before removing dependency | High | 6 |
   | [#8444](https://github.com/apache/hop/issues/8444) | Literal root-array 
JSONPath collides with `$[0]` variable escape | Add `resolveJsonPaths` default 
**true** for old pipelines; allow false in a grouped GUI settings tab, apply 
only to extraction paths | Medium | 3 |
   | [#8593](https://github.com/apache/hop/issues/8593) | Negative integers 
fail under scheduler locale | PR #8639 merged 2026-09-27; commit `18d16444b3` 
is present locally. Verify existing locale tests and release workaround; no 
JSON-local minus normalization | Low | 1 |
   
   ### Existing work that must not be duplicated
   
   - [PR #8713](https://github.com/apache/hop/pull/8713), OPEN, reviewed 
snapshot `2b6d3b1eac7f96d71b3ba9445dfb4af07e3d3b6f`, branch `2929`: enhanced 
file streaming; modifies `JsonEOutput.java`, `JsonEOutputData.java`, enhanced 
output manual, and JSON integration cases 0018/0019. GitHub returned successful 
build/UI/license/Antora checks for this snapshot. Still review behavior and 
memory guarantees locally.
   - [PR #8588](https://github.com/apache/hop/pull/8588), OPEN, reviewed 
snapshot `ed7e15e24d2603d1ca1e5c4f078f2cf578132292`, branch `5953`: common 
`ComboItems` / `PreviousFields` helpers and many dialog tests. GitHub checks 
returned success. Its comments request manual testing; a pending review flags 
successful-empty Get Fields behavior. Treat that as an explicit acceptance gate.
   - [PR #8639](https://github.com/apache/hop/pull/8639), MERGED: 
negative-number locale handling in shared `ValueMetaBase`; existing 
`ValueMetaBaseLocaleMinusTest` covers nb-NO, sv-SE, fi-FI, ar-SA, ASCII/Unicode 
minus and invalid text.
   - Timestamp guard fix is already in history: `4c889cfb01`, “fix NPE in 
Select Values when converting String to Timestamp”, #6315 / #6306. Do not add 
the same null guard again.
   
   ## Architecture / proposed approach
   
   Keep Jayway as the JSONPath evaluator and Jackson as Hop's JSON value/file 
representation; fix shared conversion and metadata-validation boundaries rather 
than applying string hacks inside individual transforms. Land independent 
correctness/documentation changes first, integrate the two existing PRs, and 
then extend the resulting file writer with an explicit NDJSON mode. Treat 
json-simple removal as its own API-sensitive migration with characterization 
tests, staged caller conversion, and packaging verification; it must not be 
bundled into the streaming or JsonPath change.
   
   ## Execution conventions (read before any task)
   
   **Timeboxes:** each numbered edit/review/test setup item is 2–5 minutes of 
focused work. A Maven build or stress run may take longer unattended. Finish 
one RED → minimal GREEN slice before adding the next test. A phase is not a 
five-minute task. Never combine all phase tests into one speculative batch 
before implementation.
   
   **Frequent commits:** commit only green slices, using explicit paths, not 
`git add .`. Commands below are for the future implementer; no commits have 
been made by the planner. Do not push or merge without authorization.
   
   **New Java files:** prepend exactly the Apache header from lines 1–16 of the 
neighboring existing Java file. The snippets below are complete class 
bodies/import sets or complete replacement members; a header is mandatory, not 
optional. Keep the package shown. Existing files keep their headers.
   
   **Fast feedback after the baseline install:** define these shell functions 
in the repository root. They are not files to add to the project.
   
   ```bash
   json_test() {
     ./mvnw -pl plugins/transforms/json -Pskip-uitest -DskipTests=false \
       -Dspotless.skip=true -Drat.skip=true -Dtest="$1" test
   }
   core_test() {
     ./mvnw -pl core -Pskip-uitest -DskipTests=false \
       -Dspotless.skip=true -Drat.skip=true -Dtest="$1" test
   }
   json_ui_test() {
     ./tools/with-isolated-display.sh ./mvnw -pl plugins/transforms/json \
       -Puitest -DskipTests=false -Dspotless.skip=true -Drat.skip=true \
       -Dtest="$1" test
   }
   ```
   
   GREEN means exit 0, `BUILD SUCCESS`, the specified class/method actually ran 
with failures/errors zero, and was **not skipped**. RED means the specified 
assertion or explicitly introduced missing API fails; missing dependencies, a 
skipped suite, or the unrelated syntax prerequisite are not the desired RED. 
Inspect 
`plugins/transforms/json/target/surefire-reports/TEST-<fully-qualified-class>.xml`
 or the corresponding `core/target/surefire-reports/` file to verify execution. 
Do not accept zero tests.
   
   ### Shared finalizer for each green slice
   
   For each task use its listed production/test/docs paths in the `git add` 
command. Run `./mvnw -pl <touched-module> spotless:apply`, rerun the focused 
tests, then `git diff --check` (expected exit 0, no output), `git diff --stat`, 
and `git commit -m '<listed message>'`. If formatting touches unrelated files, 
restore **only** formatter changes proven unrelated; do not discard user work. 
Documentation-only and already-fixed characterization tasks do not manufacture 
a failing test by breaking production code: verify the behavior, make the 
documentation/test addition, then commit.
   
   ## Phase 0 — baseline and coordinate existing work
   
   ### 0.1 Recheck checkout and prerequisites (2 minutes)
   
   Read `AGENTS.md`, `CONTRIBUTING.md`, root `pom.xml`, 
`plugins/transforms/json/pom.xml`, and `tools/with-isolated-display.sh`.
   
   ```bash
   pwd
   git status --short
   git branch --show-current
   git rev-parse HEAD
   java -version
   ./mvnw --version
   gh issue view 8711 --repo apache/hop --json body,state
   gh pr view 8588 --repo apache/hop --json state,headRefOid,mergedAt
   gh pr view 8713 --repo apache/hop --json state,headRefOid,mergedAt
   ```
   
   Expected: correct repository and Java 21; record current SHA and uncommitted 
work. PR heads/states may have changed; the hashes in this plan are evidence 
snapshots, not instructions to reset the branch. Stop rather than overwrite an 
implementer's changes. No commit for read-only preflight.
   
   ### 0.2 Establish a compilable baseline (one diagnosis slice + one repair 
slice if necessary)
   
   1. Verify the literal syntax anomaly with `git show 
HEAD:plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java`
 and search the exact token `getKeyJson...Name`. If the new baseline no longer 
contains it, do not make an unnecessary rename.
   2. Run the following compiler baseline. Expected if the token remains: 
nonzero exit, syntax error in `JsonEOutput.java` around the invalid name; 
record the actual message instead of inventing one.
   
   ```bash
   ./tools/with-isolated-display.sh ./mvnw \
     -pl plugins/transforms/json -am -Pskip-uitest \
     -DskipTests -Dspotless.skip=true -Drat.skip=true compile
   ```
   
   3. **Only if confirmed**, explicitly replace the declaration and every 
invocation of the literal token with `getKeyJsonAttributeName`. This is a 
source repair, not an assumed existing symbol. Its complete body is:
   
   ```java
   private String getKeyJsonAttributeName(JsonEOutputKeyField field) {
     String elementName = variables.resolve(field.getElementName());
     return Const.NVL(elementName, field.getFieldName());
   }
   ```
   
   Rerun the compile command; require `BUILD SUCCESS` before starting feature 
TDD. Commit the confirmed prerequisite separately:
   
   ```bash
   git add 
plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java
   git commit -m 'Restore the enhanced JSON key attribute method identifier'
   ```
   
   This is compiler RED/GREEN for a pre-existing syntax failure, not a claimed 
behavioral regression test.
   
   ### 0.3 Seed dependencies and record a real test baseline (3 minutes setup; 
build runs unattended)
   
   ```bash
   ./tools/with-isolated-display.sh ./mvnw \
     -pl plugins/transforms/json -am -Pskip-uitest -DskipTests=false \
     -Dspotless.skip=true -Drat.skip=true install
   json_test '*Test,*Tests'
   core_test 
'SimpleTimestampFormatTest,ValueMetaTimestampTest,ValueMetaBaseLocaleMinusTest'
   ```
   
   Expected: exit 0 and `BUILD SUCCESS`; save the **actual** executed counts in 
implementation/PR notes. Installation is necessary because plugin tests use 
core/engine/UI test-jars. After a later core or UI edit, reinstall that module 
and its dependencies before testing the JSON module against the new code. If 
offline artifacts are absent, resolve them normally; do not silently switch to 
`-DskipTests` for GREEN.
   
   ### 0.4 Coordinate the existing PRs (two independent 3-minute review slices)
   
   Read-only commands:
   
   ```bash
   gh pr diff 8588 --repo apache/hop --color never
   gh pr checks 8588 --repo apache/hop
   gh pr diff 8713 --repo apache/hop --color never
   gh pr checks 8713 --repo apache/hop
   ```
   
   Do not cherry-pick a whole foreign PR onto this branch as a planning 
shortcut. Once authorized and merged, update the implementation branch 
normally, then read back the target files and rebase the remaining plan. For 
#8588 inspect `ui/src/main/java/org/apache/hop/ui/core/widget/ComboItems.java`, 
`ui/src/main/java/org/apache/hop/ui/pipeline/transform/PreviousFields.java`, 
and 
`ui/src/main/java/org/apache/hop/ui/pipeline/transform/BaseTransformDialog.java`
 at the merged revision; these are **PR-introduced paths**, not files assumed 
present at this plan's baseline. Verify exact paths against `gh pr diff` before 
importing them.
   
   Acceptance: a failed lookup preserves text, raises at most one error per 
dialog; a successful lookup returning zero fields is distinguishable from 
failure and must not silently retain a stale mapping when the user explicitly 
requests replacement. Preserve JSON Input and JSON Normalize settings. No 
second sweep of unrelated dialogs under #8711.
   
   ## Phase 1 — resolve already-supported behavior and documentation
   
   ### 1.1 Verify JSON-typed source support; update instructions (#4644, 3 
minutes)
   
   Existing implementation paths:
   
   - 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInput.java:245`:
 metadata-driven String versus JSON routing.
   - 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/InputsReader.java:86`:
 `jsonFieldIterator()`.
   - 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/FastJsonReader.java:200`:
 native Jackson-node parsing.
   - `integration-tests/json/0007-json-input-json-value.hpl` and 
`integration-tests/json/main-0007-json-input-json-value.hwf`: existing 
pipeline-level coverage.
   
   Add the following characterization test to 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputTest.java`
 (existing imports/helpers suffice):
   
   ```java
   @Test
   void jsonTypedSourceFieldExtractsAValue() throws Exception {
     JsonInputField field = new JsonInputField("value");
     field.setPath("$.value");
     field.setType(IValueMeta.TYPE_STRING);
     JsonInputMeta meta = createSimpleMeta("json", field);
     meta.setRemoveSourceField(true);
     JsonNode input = HopJson.newMapper().readTree("{\"value\":\"ok\"}");
     JsonInput transform = createJsonInputWithJsonNode("json", meta, new 
Object[] {input});
     List<Object[]> rows = new ArrayList<>();
     transform.addRowListener(new RowAdapter() {
       @Override
       public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) {
         rows.add(row.clone());
       }
     });
     try {
       processRows(transform, 3);
       assertEquals(0, transform.getErrors());
       assertEquals(1, rows.size());
       assertEquals("ok", rows.getFirst()[0]);
     } finally {
       transform.dispose();
     }
   }
   ```
   
   Run `json_test 'JsonInputTest#jsonTypedSourceFieldExtractsAValue'`; expected 
GREEN **already**, because this is a stale-ticket characterization, not new 
functionality. If it fails, classify the actual failure before touching 
`StringFieldIterator`; do not serialize arbitrary Java objects with 
`toString()`.
   
   In 
`docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc`, 
replace the String-only chained-input example with:
   
   ```adoc
   * To read a nested JSON value, use either a String field containing 
serialized JSON or a JSON-typed field containing a Jackson JSON value. In the 
next JSON Input transform, enable *Source is from a previous transform* and 
select that field. Leave *Use field as file names* and *Read source as URL* 
cleared for direct JSON content. A filename or URL source remains a String 
field; an arbitrary Java Object is not a supported JSON source.
   ```
   
   Commit the exact test and manual paths, message `Document and verify 
JSON-typed JSON Input sources`.
   
   ### 1.2 Verify timestamp precision and failure propagation (#4907, three 
3-minute slices)
   
   Do not repeat the issue thread's inaccurate “nanoseconds unsupported” claim. 
Current `SimpleTimestampFormat` replaces **four or more `S` characters** with 
its fraction parser; `ValueMetaTimestampTest` already checks 6- and 9-digit 
fractions. There is no proposed `n` format letter.
   
   **Slice A:** add to 
`core/src/test/java/org/apache/hop/core/row/value/ValueMetaTimestampTest.java`:
   
   ```java
   @Test
   void parsesTheIssue4907TimestampWithAMatchingMask() throws Exception {
     ValueMetaTimestamp meta = new ValueMetaTimestamp("created");
     meta.setConversionMask("yyyy-MM-dd'T'HH:mm:ss.SSSSSS");
     assertEquals(
         Timestamp.valueOf("2025-02-13 02:10:14.746582"),
         meta.convertStringToTimestamp("2025-02-13T02:10:14.746582"));
   }
   ```
   
   Run `core_test 
'ValueMetaTimestampTest#parsesTheIssue4907TimestampWithAMatchingMask'`. 
Prospective GREEN on current source. Do not reduce precision in the assertion 
to make it pass.
   
   **Slice B:** add to 
`core/src/test/java/org/apache/hop/core/row/value/timestamp/SimpleTimestampFormatTest.java`,
 plus `import static org.junit.jupiter.api.Assertions.assertThrows;`:
   
   ```java
   @Test
   void missingRequiredOffsetIsAParseFailureNotANullPointer() {
     SimpleTimestampFormat format =
         new SimpleTimestampFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US);
     assertThrows(
         ParseException.class,
         () -> format.parse("2025-02-13T02:10:14.746582"));
   }
   ```
   
   Run `core_test 
'SimpleTimestampFormatTest#missingRequiredOffsetIsAParseFailureNotANullPointer'`;
 expected GREEN on the existing guards. If the high-precision test fails, 
reproduce it with this exact mask first; do not replace the entire date/time 
formatter under this ticket.
   
   **Slice C:** update 
`docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc` 
timestamp examples with this complete replacement text:
   
   ```adoc
   For Timestamp values, match the fraction width in the Format column. For 
example, `2025-02-13T02:10:14.746582` uses `yyyy-MM-dd'T'HH:mm:ss.SSSSSS`. 
Timestamp preserves the six fractional digits; Date has millisecond precision.
   
   `XXX` requires an offset such as `Z` or `+02:00` in the input. Do not use 
`XXX` for a value that contains no offset. A mismatched format must report a 
conversion error, not silently assume an offset or discard precision.
   ```
   
   Then manually run the JSON Input and Select Values reproductions with the 
invalid mask under an isolated GUI: both must terminate with a conversion error 
and permit the preview/dialog to close. If a freeze remains, capture the actual 
thread stacks and open a narrowly scoped follow-up; **do not close #4907 based 
on parser tests alone**. Commit each test slice separately and the 
documentation after confirmation; messages `Characterize microsecond timestamp 
conversion for issue 4907`, `Verify missing timestamp offset fails cleanly`, 
`Correct JSON Input timestamp format examples`.
   
   ### 1.3 Verify the merged locale fix (#8593, 3 minutes)
   
   ```bash
   git merge-base --is-ancestor 18d16444b3 HEAD
   core_test ValueMetaBaseLocaleMinusTest
   ```
   
   Expected ancestor command exit 0 and locale tests GREEN. No production edit. 
Verify a scheduler-style headless run with ASCII `-2` under nb-NO as part of 
Phase 7, not only in Hop GUI, and include Calculator/Filter Rows because the 
cause is shared. Existing release workaround is `HOP_OPTIONS=-Duser.language=en 
-Duser.country=US` for versions lacking the fix; do not set the user's global 
locale in code. Close only with merged SHA/release evidence and test output.
   
   ### 1.4 Correct the stale compatibility tooltip (#2572, 2 minutes)
   
   
`plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutputenhanced/messages/messages_en_US.properties`
 already says the option forces arrays for single entries. The stale version 
reference is in the Italian bundle:
   
   
`plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutputenhanced/messages/messages_it_IT.properties`
   
   Replace the exact old `JsonOutputDialog.CompatibilityMode.Tooltip` value 
with:
   
   ```properties
   JsonOutputDialog.CompatibilityMode.Tooltip=Selezionare quest''opzione per 
generare array JSON anche quando la lista contiene un solo elemento
   JsonEOutputDialog.CompatibilityMode.Tooltip=Selezionare quest''opzione per 
generare array JSON anche quando la lista contiene un solo elemento
   JsonEOutputDialog.UseArrayWihSingleInstanceMode.Tooltip=Selezionare 
quest''opzione per generare array JSON anche quando la lista contiene un solo 
elemento
   ```
   
   Retain the old key because other translations/legacy references may still 
use it; add the currently used `JsonEOutputDialog` key. Search the bundles for 
`4.3.0`; expected **no obsolete Hop-version claim** in these tooltips. Run 
`json_test JsonEOutputMetaTest` plus the final RAT/docs gates. Commit this 
bundle alone, message `Describe enhanced JSON array behavior without a 
fictitious version`.
   
   ### 1.5 Decide object ordinal access; document supported lookup (#4395, 3 
minutes)
   
   Recommend explicit maintainer approval to decline `$.[0].name()` / `$.[0]` 
on objects rather than inventing a private JSONPath dialect. The comment's 
quoted-key suggestion retrieves a known key's value; it does **not** implement 
ordinal key enumeration. Do not mark the request completed without that 
distinction.
   
   Add to 
`docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc`:
   
   ```adoc
   *Object keys and array indexes*
   
   A key containing a dot is one key, not a nested path. Use `$['doc.tipo']` to 
retrieve its value. The existing `$.keys()` function returns the object's keys 
as one JSON value; it does not define portable object-member ordering or return 
a key/value pair by ordinal.
   
   Array indexes address array elements. To select the first key/value pair by 
position, make the order explicit in the source, for example 
`[{"name":"doc.tipo","value":"XG"}]`, and use `$[0].name` and `$[0].value` with 
JsonPath variable resolution disabled. The expressions `$.[0].name()` and 
`$.[0]` do not define positional access to object members.
   ```
   
   Verification: `json_test FastJsonReaderTest` must retain function/key tests 
GREEN; inspect the rendered text. Commit only the manual, message `Clarify 
JSONPath object keys versus array indexes`. If maintainers instead require an 
ordinal feature, leave #4395 open and require a separate contract proposal 
(ordering, duplicate-key handling, out-of-range behavior, GUI), not an 
improvised parser extension.
   
   ## Phase 2 — localized correctness fixes
   
   ### 2.1 Preserve JSON null before generic String conversion (#4373)
   
   **RED (3 minutes):** create 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverterNullTest.java`,
 with the mandatory header and this complete class:
   
   ```java
   package org.apache.hop.pipeline.transforms.jsoninput.reader;
   
   import static org.junit.jupiter.api.Assertions.assertEquals;
   import static org.junit.jupiter.api.Assertions.assertNull;
   import static org.junit.jupiter.api.Assertions.assertTrue;
   import static org.mockito.Mockito.mock;
   
   import com.fasterxml.jackson.databind.node.NullNode;
   import com.fasterxml.jackson.databind.node.TextNode;
   import java.util.BitSet;
   import org.apache.hop.core.Const;
   import org.apache.hop.core.logging.ILogChannel;
   import org.apache.hop.core.row.RowMeta;
   import org.apache.hop.core.row.value.ValueMetaString;
   import org.apache.hop.pipeline.transforms.jsoninput.JsonInputData;
   import org.junit.jupiter.params.ParameterizedTest;
   import org.junit.jupiter.params.provider.ValueSource;
   
   class RowOutputConverterNullTest {
     @ParameterizedTest
     @ValueSource(strings = {"Y", "N"})
     void jsonNullIsNotConvertedIntoAnEmptyString(String distinguish) throws 
Exception {
       String before = 
System.getProperty(Const.HOP_EMPTY_STRING_DIFFERS_FROM_NULL);
       try {
         System.setProperty(Const.HOP_EMPTY_STRING_DIFFERS_FROM_NULL, 
distinguish);
         JsonInputData data = new JsonInputData();
         data.outputRowMeta = new RowMeta();
         data.outputRowMeta.addValueMeta(new ValueMetaString("nullValue"));
         data.outputRowMeta.addValueMeta(new ValueMetaString("emptyValue"));
         data.convertRowMeta = 
data.outputRowMeta.cloneToType(ValueMetaString.TYPE_STRING);
         data.totalpreviousfields = 0;
         data.repeatedFields = new BitSet();
         RowOutputConverter converter = new 
RowOutputConverter(mock(ILogChannel.class));
         Object[] row = converter.getRow(
             new Object[0],
             new Object[] {NullNode.getInstance(), TextNode.valueOf("")},
             data);
         assertNull(row[0]);
         assertEquals("", row[1]);
         if ("Y".equals(distinguish)) {
           assertTrue(!data.outputRowMeta.getValueMeta(1).isNull(row[1]));
         }
       } finally {
         if (before == null) {
           System.clearProperty(Const.HOP_EMPTY_STRING_DIFFERS_FROM_NULL);
         } else {
           System.setProperty(Const.HOP_EMPTY_STRING_DIFFERS_FROM_NULL, before);
         }
       }
     }
   }
   ```
   
   `JsonInputData.repeatedFields` was inspected at 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputData.java:58`:
 its declared type is `BitSet`, so the initialization above matches the real 
API.
   
   Run `json_test RowOutputConverterNullTest`. Expected RED with `Y`: actual 
null column is `""`, because `ValueMetaBase.convertDataFromString` rewrites 
null to `Const.NULL_STRING` under that setting. `N` is the compatibility case 
and should remain green.
   
   **GREEN (2 minutes):** in 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/reader/RowOutputConverter.java`,
 immediately after `String strValue = getStringValue(value);`, insert:
   
   ```java
   if (strValue == null) {
     return null;
   }
   ```
   
   This preserves the JSON parser's null/NullNode/MissingNode result before 
text-source conventions change it. Keep numeric conversion, trim processing, 
and repeated-field handling unchanged. Run `json_test 
'RowOutputConverterNullTest,RowOutputConverterTest,JsonInputTest'`; expected 
GREEN. Add separate raw-null/MissingNode/repeat cases only after this first 
slice is green; do not change `ValueMetaBase` globally. Commit the converter 
and new test, message `Preserve JSON null when empty strings differ from null`.
   
   ### 2.2 Validate absent group keys in metadata (#2570, one 3-minute RED + 
one 3-minute GREEN slice)
   
   In 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutputMetaTest.java`,
 add imports `assertThrows`, 
`org.apache.hop.core.exception.HopTransformException`, 
`org.apache.hop.core.row.RowMeta`, 
`org.apache.hop.core.row.value.ValueMetaString`, 
`org.apache.hop.core.variables.Variables`, and `java.util.List`. Add:
   
   ```java
   @Test
   void missingGroupKeyNamesTheMissingField() {
     JsonEOutputMeta meta = new JsonEOutputMeta();
     meta.setOperationType(JsonEOutputMeta.OperationType.OUTPUT_VALUE);
     meta.setKeyFields(List.of(new JsonEOutputKeyField("Field1")));
     RowMeta row = new RowMeta();
     row.addValueMeta(new ValueMetaString("Field2"));
     HopTransformException error = assertThrows(
         HopTransformException.class,
         () -> meta.getFields(row, "json", null, null, new Variables(), new 
MemoryMetadataProvider()));
     assertTrue(error.getMessage().contains("Field1"));
   }
   ```
   
   Run `json_test 'JsonEOutputMetaTest#missingGroupKeyNamesTheMissingField'`; 
prospective RED is the wrong exception or no useful field-named exception.
   
   In 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutputMeta.java`,
 replace the lookup/add pair inside its `getFields()` key loop with:
   
   ```java
   int index = rowMeta.indexOfValue(keyField.getFieldName());
   if (index < 0) {
     throw new HopTransformException(
         BaseMessages.getString(PKG, "JsonEOutput.Error.GroupFieldNotFound", 
keyField.getFieldName()));
   }
   IValueMeta vmi = rowMeta.getValueMeta(index).clone();
   row.addValueMeta(vmi);
   ```
   
   Add to 
`plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsonoutputenhanced/messages/messages_en_US.properties`:
   
   ```properties
   JsonEOutput.Error.GroupFieldNotFound=Group key field ''{0}'' was not found 
in the incoming row.
   ```
   
   Run `json_test JsonEOutputMetaTest`; expected GREEN. Commit meta, test and 
bundle, message `Validate enhanced JSON group keys during metadata discovery`.
   
   ### 2.3 Validate the same key before runtime metadata access (#2570, next 
vertical slice)
   
   The runtime 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java:onFirstRecord`
 performs the same unsafe lookup **before** `initKeyFieldsPositionArray`. Move 
validation before that loop; do not catch an `IndexOutOfBoundsException` after 
the fact. The test harness and full runtime regression code are supplied in 
Phase 4's test appendix, which is usable before streaming integration.
   
   RED: `json_test 
'JsonEOutputBehaviorTest#missingRuntimeGroupKeyIsAnActionableError'`; expected 
the original index failure rather than a `HopException` naming the test's 
configured key `gone`.
   
   Minimal replacement inside the runtime key loop:
   
   ```java
   int index = data.inputRowMeta.indexOfValue(keyFields.get(i).getFieldName());
   if (index < 0) {
     throw new HopException(
         BaseMessages.getString(PKG, "JsonEOutput.Error.GroupFieldNotFound", 
keyFields.get(i).getFieldName()));
   }
   
data.outputRowMeta.addValueMeta(data.inputRowMeta.getValueMeta(index).clone());
   ```
   
   Run the runtime test and `json_test 
'JsonEOutputMetaTest,JsonEOutputBehaviorTest'`; require GREEN. Commit 
runtime/test paths, message `Report missing enhanced JSON group keys before 
runtime lookup`.
   
   ### 2.4 Preserve metadata on failure and share group-key validation (next 
3-minute RED/GREEN slice)
   
   After 2.2/2.3, add Appendix A's `missingKeyIsNamedDuringSchemaDiscovery`. 
Run `json_test 
'JsonEOutputBehaviorTest#missingKeyIsNamedDuringSchemaDiscovery'`; expected RED 
because the existing `getFields()` clears the caller's row before discovering 
that a key is missing. This is a separate defect from exception naming.
   
   In 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutputMeta.java`,
 add the complete shared resolver:
   
   ```java
   public int[] resolveKeyFieldIndexes(IRowMeta row) throws 
HopTransformException {
     int[] indexes = new int[keyFields.size()];
     for (int i = 0; i < keyFields.size(); i++) {
       String name = keyFields.get(i).getFieldName();
       indexes[i] = row.indexOfValue(name);
       if (indexes[i] < 0) {
         throw new HopTransformException(
             BaseMessages.getString(PKG, 
"JsonEOutput.Error.GroupFieldNotFound", name));
       }
     }
     return indexes;
   }
   ```
   
   At the start of `getFields()`'s non-file-only branch, replace its 
`rowMeta`/clear/key-loop portion with:
   
   ```java
   IRowMeta rowMeta = row.clone();
   int[] keyIndexes = resolveKeyFieldIndexes(rowMeta);
   row.clear();
   for (int keyIndex : keyIndexes) {
     row.addValueMeta(rowMeta.getValueMeta(keyIndex).clone());
   }
   ```
   
   Retain the following generated-JSON/size metadata. In 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java:onFirstRecord()`,
 immediately after `data.inputRowMetaSize` is set, assign 
`data.keysGroupIndexes = meta.resolveKeyFieldIndexes(data.inputRowMeta);`. 
Replace the key-metadata loop with:
   
   ```java
   for (int i = 0; i < meta.getKeyFields().size(); i++) {
     
data.outputRowMeta.addValueMeta(data.inputRowMeta.getValueMeta(data.keysGroupIndexes[i]).clone());
   }
   ```
   
   Replace the final `if (initKeyFieldsPositionArray(r)) return true; return 
false;` with `return false;` and remove the now-unused 
`initKeyFieldsPositionArray` method. Do not retain two competing key-index 
resolvers. Run the same metadata-preservation test plus the runtime test; 
require GREEN.
   
   For pipeline Validate, add the same resolver call inside 
`JsonEOutputMeta.check()`'s existing `prev != null && !prev.isEmpty()` branch:
   
   ```java
   try {
     resolveKeyFieldIndexes(prev);
   } catch (HopTransformException e) {
     remarks.add(new CheckResult(ICheckResult.TYPE_RESULT_ERROR, 
e.getMessage(), transformMeta));
   }
   ```
   
   Add to 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutputMetaTest.java`
 (existing imports plus `java.util.ArrayList`, 
`org.apache.hop.core.ICheckResult`):
   
   ```java
   @Test
   void validationReportsAMissingGroupKey() {
     JsonEOutputMeta meta = new JsonEOutputMeta();
     meta.setKeyFields(List.of(new JsonEOutputKeyField("gone")));
     RowMeta previous = new RowMeta();
     previous.addValueMeta(new ValueMetaString("payload"));
     List<ICheckResult> remarks = new ArrayList<>();
     meta.check(remarks, null, new TransformMeta("EnhancedJsonOutput", "json", 
meta),
         previous, new String[] {"upstream"}, new String[0], null,
         new Variables(), new MemoryMetadataProvider());
     assertTrue(remarks.stream().anyMatch(result ->
         result.getType() == ICheckResult.TYPE_RESULT_ERROR && 
result.getText().contains("gone")));
   }
   ```
   
   Run `json_test 'JsonEOutputMetaTest#validationReportsAMissingGroupKey'` 
before adding the check (RED, no named key remark), then after (GREEN). Commit 
this shared validation/preservation slice separately, using both production 
files, both test files and the existing message bundle; message `Share enhanced 
JSON key validation without clearing metadata on failure`.
   
   ## Phase 3 — small GUI-backed features, after the UI helper PR
   
   ### 3.1 Literal root-array paths (#8444): failing transform test (3 minutes)
   
   Add this complete method to 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputTest.java`.
 Use the existing imports and helpers. Add the configuration flag in 3.2, then 
run the test **before** changing `createReader()`, so the behavioral RED is 
visible rather than only a missing-getter compiler error.
   
   ```java
   @Test
   void literalRootArrayPathExtractsTheFirstValue() throws Exception {
     String path = "$[0].similarity";
     String input = "[{\"Id\":3902113,\"similarity\":0.95}]";
     JsonInputField field = new JsonInputField("similarity");
     field.setPath(path);
     field.setType(IValueMeta.TYPE_NUMBER);
     JsonInputMeta meta = createSimpleMeta("json", field);
     meta.setResolveJsonPaths(false);
     meta.setRemoveSourceField(true);
     JsonInput transform = createJsonInput("json", meta, new Object[] {input});
     List<Object[]> rows = new ArrayList<>();
     transform.addRowListener(new RowAdapter() {
       @Override
       public void rowWrittenEvent(IRowMeta rowMeta, Object[] row) {
         rows.add(row.clone());
       }
     });
     try {
       assertEquals(0, transform.getErrors(), "literal JSONPath must compile");
       processRows(transform, 3);
       assertEquals(0, transform.getErrors());
       assertEquals(1, rows.size());
       assertEquals(0.95d, rows.getFirst()[0]);
       assertEquals(path, meta.getInputFields().getFirst().getPath());
     } finally {
       transform.dispose();
     }
   }
   ```
   
   Command: `json_test 
'JsonInputTest#literalRootArrayPathExtractsTheFirstValue'`. Expected behavioral 
RED: the old `resolve()` transforms `$[0]` into a control character, so 
initialization/extraction fails.
   
   ### 3.2 Persist the backwards-compatible flag and gate only JsonPath 
resolution (two 3-minute slices)
   
   In 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputMeta.java`,
 add the following imports, `@GuiPlugin` to the class, and members. Do **not** 
change the transform ID `JsonInput`.
   
   ```java
   import org.apache.hop.core.gui.plugin.GuiElementType;
   import org.apache.hop.core.gui.plugin.GuiPlugin;
   import org.apache.hop.core.gui.plugin.GuiWidgetElement;
   import org.apache.hop.core.gui.plugin.GuiWidgetGroupType;
   ```
   
   ```java
   public static final String GUI_PLUGIN_ELEMENT_PARENT_ID = 
"JsonInput.JsonPathOptions";
   
   @HopMetadataProperty(
       key = "resolveJsonPaths",
       defaultBoolean = true,
       injectionKey = "RESOLVE_JSON_PATHS",
       injectionKeyDescription = "JsonInput.Injection.RESOLVE_JSON_PATHS")
   @GuiWidgetElement(
       id = "resolveJsonPaths",
       order = "0100",
       type = GuiElementType.CHECKBOX,
       label = "i18n::JsonInputMeta.ResolveJsonPaths.Label",
       toolTip = "i18n::JsonInputMeta.ResolveJsonPaths.Tooltip",
       parentId = GUI_PLUGIN_ELEMENT_PARENT_ID,
       groupType = GuiWidgetGroupType.BOXES,
       group = "JSONPath",
       groupOrder = "0100")
   private boolean resolveJsonPaths = true;
   ```
   
   Lombok already exists on this class. `defaultBoolean=true` is essential: 
metadata deserialization applies annotation defaults, not just constructor 
defaults. Missing legacy XML/JSON property must retain **true**.
   
   Add to 
`plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/jsoninput/messages/messages_en_US.properties`:
   
   ```properties
   JsonInputMeta.ResolveJsonPaths.Label=Resolve variables in JSONPath
   JsonInputMeta.ResolveJsonPaths.Tooltip=Clear this option to use extraction 
paths literally, including root array indexes such as '$[0]'. When selected, 
existing variable expressions such as '${JSON_PATH}' are resolved.
   JsonInput.Injection.RESOLVE_JSON_PATHS=Resolve variables in JSONPath 
expressions? (Y/N)
   JsonInputDialog.PathOptions.TabTitle=JSONPath
   ```
   
   Then, in 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInput.java:createReader()`,
 replace only the unconditional `field.setPath(resolve(field.getPath()));` with:
   
   ```java
   if (meta.isResolveJsonPaths()) {
     field.setPath(resolve(field.getPath()));
   }
   ```
   
   Keep its defensive `new JsonInputField(...)` copy. Do not change file/URL 
resolution or `Variables.resolve()` globally. Run:
   
   ```bash
   json_test 'JsonInputTest#literalRootArrayPathExtractsTheFirstValue'
   json_test 'JsonInputTest#testJsonInputPathResolutionSuccess'
   json_test 'JsonInputTest#testJsonInputMetaInputFieldsNotOverwritten'
   ```
   
   Expected GREEN for all three, including unchanged `${PARAM_PATH}` behavior 
with default true. Stage the runtime, meta, test and bundle paths; commit 
message `Allow literal JSONPath expressions without changing legacy resolution` 
**only after the GUI slice below is green**. The test-first flag/runtime/GUI 
work is one vertical feature; do not ship a file-only setting.
   
   ### 3.3 Put the new flag in one grouped tab, not a whole-dialog redesign (3 
minutes)
   
   In 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputDialog.java`,
 add imports:
   
   ```java
   import org.apache.hop.ui.core.gui.GuiCompositeWidgets;
   import org.apache.hop.ui.core.gui.GuiCompositeWidgetsAdapter;
   ```
   
   Add the field and complete method:
   
   ```java
   private GuiCompositeWidgets pathWidgets;
   
   private void addPathOptionsTab() {
     CTabItem tab = new CTabItem(wTabFolder, SWT.NONE);
     tab.setText(BaseMessages.getString(PKG, 
"JsonInputDialog.PathOptions.TabTitle"));
     Composite parent = new Composite(wTabFolder, SWT.NONE);
     PropsUi.setLook(parent);
     parent.setLayout(new FormLayout());
     pathWidgets = GuiCompositeWidgets.addScrolledComposite(
         parent, variables, null, null, 
JsonInputMeta.GUI_PLUGIN_ELEMENT_PARENT_ID, input);
     pathWidgets.setWidgetsListener(new GuiCompositeWidgetsAdapter() {
       @Override
       public void widgetModified(GuiCompositeWidgets widgets, Control control, 
String widgetId) {
         input.setChanged();
       }
     });
     tab.setControl(parent);
   }
   ```
   
   Call `addPathOptionsTab();` immediately after `addContentTab();` in 
`open()`. At the start of `getInfo(JsonInputMeta in)` add:
   
   ```java
   pathWidgets.getWidgetsContents(in, 
JsonInputMeta.GUI_PLUGIN_ELEMENT_PARENT_ID);
   ```
   
   The existing OK/Cancel/Preview bar stays pinned below `wTabFolder`; do not 
add shell-level `FormAttachment` rows. Preview uses `getInfo(oneMeta)`, so it 
receives the flag too.
   
   **GUI RED/GREEN:** create 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputPathOptionsDialogTest.java`,
 mandatory header plus:
   
   ```java
   package org.apache.hop.pipeline.transforms.jsoninput;
   
   import static org.junit.jupiter.api.Assertions.assertFalse;
   import static org.junit.jupiter.api.Assertions.assertTrue;
   
   import java.lang.reflect.Field;
   import org.apache.hop.core.gui.plugin.GuiRegistry;
   import org.apache.hop.core.gui.plugin.GuiWidgetElement;
   import org.apache.hop.core.variables.Variables;
   import org.apache.hop.i18n.BaseMessages;
   import org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider;
   import org.apache.hop.pipeline.PipelineMeta;
   import org.apache.hop.pipeline.transform.TransformMeta;
   import org.apache.hop.ui.testing.SwtBotTestBase;
   import org.junit.jupiter.api.BeforeAll;
   import org.junit.jupiter.api.Tag;
   import org.junit.jupiter.api.Test;
   
   @Tag("uitest")
   class JsonInputPathOptionsDialogTest extends SwtBotTestBase {
     @BeforeAll
     static void registerWidgets() {
       GuiRegistry registry = GuiRegistry.getInstance();
       for (Field field : JsonInputMeta.class.getDeclaredFields()) {
         GuiWidgetElement element = field.getAnnotation(GuiWidgetElement.class);
         if (element != null) {
           registry.addGuiWidgetElement(JsonInputMeta.class.getName(), element, 
field);
         }
       }
     }
   
     private PipelineMeta pipeline(JsonInputMeta meta) {
       PipelineMeta pipeline = new PipelineMeta();
       pipeline.setMetadataProvider(new MemoryMetadataProvider());
       pipeline.addTransform(new TransformMeta("JsonInput", "json", meta));
       return pipeline;
     }
   
     @Test
     void okPersistsLiteralPathMode() {
       JsonInputMeta meta = new JsonInputMeta();
       PipelineMeta pipeline = pipeline(meta);
       withDialog(
           parent -> new JsonInputDialog(parent, new Variables(), meta, 
pipeline).open(),
           bot -> {
             var dialog = bot.shell(BaseMessages.getString(JsonInputMeta.class,
                 "JsonInputDialog.DialogTitle")).activate().bot();
             dialog.cTabItem("JSONPath").activate();
             dialog.checkBox("Resolve variables in JSONPath").deselect();
             dialog.button(buttonLabel("System.Button.OK")).click();
           });
       assertFalse(meta.isResolveJsonPaths());
     }
   
     @Test
     void cancelDoesNotPersistLiteralPathMode() {
       JsonInputMeta meta = new JsonInputMeta();
       PipelineMeta pipeline = pipeline(meta);
       withDialog(
           parent -> new JsonInputDialog(parent, new Variables(), meta, 
pipeline).open(),
           bot -> {
             var dialog = bot.shell(BaseMessages.getString(JsonInputMeta.class,
                 "JsonInputDialog.DialogTitle")).activate().bot();
             dialog.cTabItem("JSONPath").activate();
             dialog.checkBox("Resolve variables in JSONPath").deselect();
             dialog.button(buttonLabel("System.Button.Cancel")).click();
           });
       assertTrue(meta.isResolveJsonPaths());
     }
   }
   ```
   
   Run `json_ui_test JsonInputPathOptionsDialogTest` before adding the tab 
(expected missing-tab/checkbox failure), and again after (expected GREEN). Run 
in the normal en-US test environment used by existing UI tests; translate 
lookup labels with `BaseMessages` if this environment is customized. Require 
both tests to execute, not be headless-skipped.
   
   ### 3.4 Verify old and new serialization behavior (3 minutes)
   
   In 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputMetaTest.java`,
 add XML round-trip cases following its existing `XmlMetadataUtil` pattern, 
with this complete reusable method in `JsonInputTest.java` to test the 
**behavior** of a deserialized setting (existing imports plus `XmlHandler`, 
`XmlMetadataUtil`, `MemoryMetadataProvider`, `TransformMeta`):
   
   ```java
   @Test
   void serializedLiteralPathModeStillExtractsRootArray() throws Exception {
     JsonInputMeta original = new JsonInputMeta();
     original.setResolveJsonPaths(false);
     String xml = "<transform>" +
         
org.apache.hop.metadata.serializer.xml.XmlMetadataUtil.serializeObjectToXml(original)
 +
         "</transform>";
     JsonInputMeta loaded = new JsonInputMeta();
     org.apache.hop.metadata.serializer.xml.XmlMetadataUtil.deSerializeFromXml(
         org.apache.hop.core.xml.XmlHandler.loadXmlString(xml,
             org.apache.hop.pipeline.transform.TransformMeta.XML_TAG),
         JsonInputMeta.class, loaded,
         new 
org.apache.hop.metadata.serializer.memory.MemoryMetadataProvider());
     assertFalse(loaded.isResolveJsonPaths());
     JsonInputField field = new JsonInputField("value");
     field.setPath("$[0].value");
     field.setType(IValueMeta.TYPE_STRING);
     loaded.setInputFields(new ArrayList<>(List.of(field)));
     loaded.setInFields(true);
     loaded.setFieldValue("json");
     loaded.setRemoveSourceField(true);
     JsonInput transform = createJsonInput("json", loaded,
         new Object[] {"[{\"value\":\"ok\"}]"});
     RowComparatorListener rows = new RowComparatorListener(new Object[] 
{"ok"});
     transform.addRowListener(rows);
     try {
       processRows(transform, 3);
       assertEquals(0, transform.getErrors());
       assertEquals(1, rows.rowNbr);
     } finally {
       transform.dispose();
     }
   }
   ```
   
   For absent-property default, deserialize `<transform><fields/></transform>` 
into a fresh meta and assert `isResolveJsonPaths()` is true, then run the 
already-supplied `${PARAM_PATH}` behavior test. Command `json_test 
'JsonInputMetaTest,JsonInputTest'`; expected GREEN. Commit the extra 
persistence test separately, message `Verify JSONPath mode survives pipeline 
serialization`.
   
   Update 
`docs/hop-user-manual/modules/ROOT/pages/pipeline/transforms/jsoninput.adoc` 
with:
   
   ```adoc
   === JSONPath tab
   
   *Resolve variables in JSONPath* is enabled by default, including for 
existing pipelines. Clear it to use every extraction Path literally. This is 
required for root-array paths such as `$[0].similarity`, whose `$[0]` otherwise 
has a meaning in Hop variable resolution. Clearing it also disables 
`${VARIABLE}` substitution inside extraction paths. File and URL settings 
continue to resolve variables normally.
   ```
   
   ### 3.5 Verify #5953 on the existing PR rather than implementing it again (3 
minutes)
   
   After #8588 has been approved and integrated, run:
   
   ```bash
   json_ui_test 
'JsonInputDialogKeepsValuesTest,JsonNormalizeInputDialogKeepsValuesTest'
   ./tools/with-isolated-display.sh ./mvnw -pl rcp -Puitest \
     -DskipTests=false '-Dtest=ComboItemsTest,PreviousFieldsTest' test
   ```
   
   Exact introduced test paths:
   
   - 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsoninput/JsonInputDialogKeepsValuesTest.java`
   - 
`plugins/transforms/json/src/test/java/org/apache/hop/pipeline/transforms/jsonnormalize/JsonNormalizeInputDialogKeepsValuesTest.java`
   - `rcp/src/test/java/org/apache/hop/ui/core/widget/ComboItemsTest.java`
   - 
`rcp/src/test/java/org/apache/hop/ui/pipeline/transform/PreviousFieldsTest.java`
   
   Expected GREEN after integration; error shown once and `id_json` still saved 
on OK. Test a successful **empty** upstream separately from a failing upstream; 
`PreviousFields.isAvailable()` distinguishes them. The source field combo must 
retain user text in both cases, but an explicit grid-replacement command must 
follow the agreed empty-result policy. Do not paper over failed UI tests by 
clearing fields.
   
   ## Phase 4 — enhanced-output grouping, Get Fields, and streaming PR 
acceptance
   
   The next section supplies complete proposed code/test members for 
#5167/#5313/#4344 and a shared behavioral harness. Source APIs for grouped 
widgets and button listeners were independently read; all snippets remain 
uncompiled in this planning turn. Copy **one test method plus its harness** for 
the current vertical slice, not all future tests at once.
   
   ### 4.1 Adopt the streaming PR with explicit acceptance gates (five 3-minute 
setup/review slices)
   
   **4.1a — file contract review:** read 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutput.java`
 and 
`plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/jsonoutputenhanced/JsonEOutputData.java`
 from integrated #8713. Confirm `streamFileRows`, `collectGroupItems`, 
`pendingFileItem`, `fileItemCount`, `fileGenerator`, `finishGroup`, 
`writeFileItem`, `finishFile` match the reviewed snapshot; re-plan this section 
if they do not. Without keys or forced merging, file-only output should retain 
at most one pending item plus the row being written. Grouped output still 
retains the current group; BOTH and forced merging are not constant-memory 
promises.
   
   **4.1b — correctness RED/GREEN:** install only Appendix A's harness plus 
`bothKeepsRowShapeAndWritesEveryGroup`. Run `json_test 
'JsonEOutputBehaviorTest#bothKeepsRowShapeAndWritesEveryGroup'`. On the 
reviewed PR this is expected to fail because `outputRow()` always appends two 
cells even when its metadata has no JSON-size field. Replace its 
allocation/population block with:
   
   ```java
   Object[] additionalRowFields =
       Utils.isEmpty(meta.getJsonSizeFieldName())
           ? new Object[] {data.jsonSerialized}
           : new Object[] {data.jsonSerialized, data.jsonLength};
   ```
   
   Keep `RowDataUtil.addRowData`, counters and `putRow` unchanged. Rerun the 
same test; require GREEN and all groups present in the file. Commit runtime and 
Appendix A's test path, message `Match enhanced JSON output row width to its 
metadata`.
   
   **4.1c — PR fixture validation:** use the exact PR-introduced files, do not 
create a parallel streaming implementation:
   
   - `integration-tests/json/0018-enhanced-json-output-streaming.hpl`
   - `integration-tests/json/main-0018-enhanced-json-output-streaming.hwf`
   - `integration-tests/json/0019-enhanced-json-output-grouped-file.hpl`
   - `integration-tests/json/main-0019-enhanced-json-output-grouped-file.hwf`
   
   Run the integration command in Phase 7 with 
`TEST_FILTER='0018-enhanced-json-output-streaming,0019-enhanced-json-output-grouped-file'`;
 expected both workflows recorded as passed. #8713 deliberately changes split 
counting to complete file items: rows without keys, groups with keys; never cut 
one group across files. Review and disclose this compatibility change.
   
   **4.1d — memory evidence:** the reviewed 0018 file is 126,888,906 bytes 
while `-Xmx128m` is 134,217,728 bytes. It catches retained-tree overhead, but 
is not a file larger than the entire maximum heap. Correct the inaccurate 
“several times larger” claim in 
`integration-tests/json/0018-enhanced-json-output-streaming.hpl`, or strengthen 
the existing workflow with the exact larger-row calculation in Phase 7. Do not 
claim bounded memory for an unlimited single group. Cancellation/write-error 
cleanup must close the generator before the writer; Appendix C supplies a test 
and minimal cleanup.
   
   **4.1e — #2569 attachment:** attempted read-only retrieval of 
`https://github.com/apache/hop/files/10979443/json.enhanced.test.zip` was 
blocked by an unanswered tool approval, so its contents are **not verified**. 
Obtain the attachment from the user/maintainer with approval during 
implementation; do not bypass the approval or guess its settings. Open its 
`.hpl` in the isolated GUI, run unchanged before/after the integrated PR, 
compare parsed JSON (not whitespace), and preserve it as a regression fixture 
only if it exposes behavior not covered by 0019. Leave #2569 open until the 
sample-specific result is recorded.
   
   **Commit discipline for #5167:** first add Appendix A's 
`keySuggestionsExcludeLiveOutputAndExistingKeys` and run `json_test 
'JsonEOutputBehaviorTest#keySuggestionsExcludeLiveOutputAndExistingKeys'` 
(initial missing-helper compiler RED); add only `suggestKeyFieldNames`, rerun 
(GREEN). Then add the real GUI test in Appendix C and observe 
missing-button/geometry RED; wire the grouped button/table below and rerun GUI 
+ selection test. Commit the meta, dialog, GUI/behavior tests, and message 
bundle, message `Offer unmapped incoming fields as enhanced JSON group keys`. 
Do not use stale `input.getOutputFields()` to exclude unsaved live table 
selections. Existing key aliases must survive repeat clicks and Cancel must not 
mutate the metadata list.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to