This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 5c130f941 WW-5641 Restore struts.json.writer / struts.json.reader 
override in JSON plugin (#1766)
5c130f941 is described below

commit 5c130f9411a5c8f5c8bbb60744e25ce6d31ddd9b
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sun Jul 12 11:29:41 2026 +0200

    WW-5641 Restore struts.json.writer / struts.json.reader override in JSON 
plugin (#1766)
    
    * WW-5641 docs: design spec for JSON writer/reader override regression
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    * WW-5641 docs: implementation plan for JSON writer/reader override fix
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    * WW-5641 fix: run JSON bean-selection from struts-deferred.xml
    
    The JSON plugin declared <bean-selection> in struts-plugin.xml, which runs
    at plugin-parse time, before the application struts.xml is folded in. That
    froze the JSONWriter/JSONReader default binding to StrutsJSONWriter/Reader,
    so struts.json.writer / struts.json.reader overrides were ignored.
    
    Move the element to struts-deferred.xml, which Dispatcher loads last (after
    the app config and core's StrutsBeanSelectionProvider), so the alias honors
    the override. Mirrors the velocity plugin. JSONUtil is unchanged from main.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 ...26-07-06-WW-5641-json-writer-reader-override.md | 447 +++++++++++++++++++++
 ...son-writer-reader-override-regression-design.md | 161 ++++++++
 .../json/src/main/resources/struts-deferred.xml    |  30 ++
 plugins/json/src/main/resources/struts-plugin.xml  |   2 -
 .../apache/struts2/json/CustomTestJSONReader.java  |  39 ++
 .../apache/struts2/json/CustomTestJSONWriter.java  |  51 +++
 .../struts2/json/JSONWriterOverrideTest.java       |  78 ++++
 .../src/test/resources/struts-json-override.xml    |  32 ++
 8 files changed, 838 insertions(+), 2 deletions(-)

diff --git 
a/docs/superpowers/plans/2026-07-06-WW-5641-json-writer-reader-override.md 
b/docs/superpowers/plans/2026-07-06-WW-5641-json-writer-reader-override.md
new file mode 100644
index 000000000..5b36e0bf6
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-06-WW-5641-json-writer-reader-override.md
@@ -0,0 +1,447 @@
+# WW-5641: Restore `struts.json.writer` / `struts.json.reader` Override — 
Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use 
superpowers:subagent-driven-development (recommended) or 
superpowers:executing-plans to implement this plan task-by-task. Steps use 
checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Restore the documented ability to override the JSON 
serializer/deserializer via the `struts.json.writer` / `struts.json.reader` 
constants, which regressed in the 7.2.x JSON hardening rework.
+
+**Architecture:** The JSON plugin's `<bean-selection>` runs at plugin-parse 
time (inside `struts-plugin.xml`), before the app config is folded in, so it 
freezes the default `JSONWriter`/`JSONReader` binding to the shipped defaults. 
The framework already provides the order-safe home for order-sensitive 
bean-selection: `struts-deferred.xml`, which `Dispatcher.init()` loads 
**last**. The fix moves the JSON `<bean-selection>` from `struts-plugin.xml` 
into a new `struts-deferred.xml` (mirrori [...]
+
+**Tech Stack:** Java, Maven, Struts internal IoC container, JUnit 
(`junit.framework.TestCase` via `StrutsTestCase`).
+
+**Spec:** 
`docs/superpowers/specs/2026-07-06-WW-5641-json-writer-reader-override-regression-design.md`
+**Ticket:** [WW-5641](https://issues.apache.org/jira/browse/WW-5641)
+
+## Global Constraints
+
+- Commit messages MUST be prefixed with the ticket id: `WW-5641 <type>: 
<desc>`.
+- Work on the feature branch `WW-5641-json-writer-reader-override`. Never 
commit to `main`.
+- Do NOT change `StrutsJSONWriter`, `StrutsJSONReader`, the DoS-limit 
constants (`struts.json.maxDepth`, `maxElements`, `maxLength`, 
`maxStringLength`, `maxKeyLength`), `JSONBeanSelectionProvider`, 
`AbstractBeanSelectionProvider`, or any core class.
+- `JSONUtil.java` must end up byte-identical to its state on `main` (commit 
`789dbf3cd`): `@Inject` on `setWriter(JSONWriter)` and `setReader(JSONReader)`, 
no `setContainer`, no `org.apache.struts2.inject.Container` import.
+- The `<bean name="struts">` writer/reader definitions, the 
`JSONUtil`/`JSONCacheDestroyable` beans, all constants, and the `json-default` 
package stay in `struts-plugin.xml` — mirror the velocity plugin, which keeps 
`<bean>` in `struts-plugin.xml` and only `<bean-selection>` in 
`struts-deferred.xml`.
+- Test idiom for this module: extend `StrutsTestCase`, name tests `testXxx()`, 
use inherited `assertEquals` (JUnit 3/4 style). No `@Test` annotations.
+- The test `config` init param must be 
`struts-default.xml,struts-plugin.xml,struts-json-override.xml` — do NOT append 
`struts-deferred.xml` (the framework loads it unconditionally after the chain; 
listing it twice emits a spurious warning).
+
+---
+
+### Task 1: Move the JSON `<bean-selection>` to `struts-deferred.xml`, revert 
`JSONUtil`, prove the override wins (RED → GREEN)
+
+One task: revert the earlier (rejected) `JSONUtil` change, relocate the 
`<bean-selection>` element, add the regression test, and verify RED→GREEN plus 
the full module suite. Single green commit.
+
+**Files:**
+- Modify: `plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java` 
(revert to `main` state — remove `setContainer` + `Container` import, restore 
`@Inject` on the two setters)
+- Modify: `plugins/json/src/main/resources/struts-plugin.xml` (delete the 
`<bean-selection>` line)
+- Create: `plugins/json/src/main/resources/struts-deferred.xml` (the moved 
`<bean-selection>`)
+- Create: 
`plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java`
+- Create: 
`plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java`
+- Create: `plugins/json/src/test/resources/struts-json-override.xml`
+- Create: 
`plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java`
+
+**Interfaces:**
+- Consumes: `JSONWriter` (methods: `String write(Object)`; `String 
write(Object, Collection<Pattern>, Collection<Pattern>, boolean)`; 
`setIgnoreHierarchy(boolean)`; `setEnumAsBean(boolean)`; 
`setDateFormatter(String)`; `setCacheBeanInfo(boolean)`; 
`setExcludeProxyProperties(boolean)`). `JSONReader` (methods: `Object 
read(String)`; `setMaxElements(int)`; `setMaxDepth(int)`; 
`setMaxStringLength(int)`; `setMaxKeyLength(int)`). `JSONUtil.serialize(Object, 
boolean)` and `JSONUtil.getReader()` [...]
+- Produces: no new API. The behavioral contract: 
`container.getInstance(JSONWriter.class)` / `getInstance(JSONReader.class)` 
resolve to the app override when configured.
+
+- [ ] **Step 1: Revert the `JSONUtil` import**
+
+In `plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java`, remove 
the `Container` import line so only the `Inject` import remains:
+
+```java
+import org.apache.struts2.inject.Inject;
+```
+
+(Delete the `import org.apache.struts2.inject.Container;` line directly above 
it.)
+
+- [ ] **Step 2: Revert the `JSONUtil` setters to the `main` state**
+
+Replace the current block (the `setContainer` method plus the two plain 
setters):
+
+```java
+    @Inject
+    public void setContainer(Container container) {
+        setWriter(container.getInstance(JSONWriter.class,
+                container.getInstance(String.class, 
JSONConstants.JSON_WRITER)));
+        setReader(container.getInstance(JSONReader.class,
+                container.getInstance(String.class, 
JSONConstants.JSON_READER)));
+    }
+
+    public void setReader(JSONReader reader) {
+        this.reader = reader;
+    }
+
+    public JSONReader getReader() {
+        return reader;
+    }
+
+    public void setWriter(JSONWriter writer) {
+        this.writer = writer;
+    }
+```
+
+with the original `main` version (restore `@Inject` on both setters, no 
`setContainer`):
+
+```java
+    @Inject
+    public void setReader(JSONReader reader) {
+        this.reader = reader;
+    }
+
+    public JSONReader getReader() {
+        return reader;
+    }
+
+    @Inject
+    public void setWriter(JSONWriter writer) {
+        this.writer = writer;
+    }
+```
+
+Verify with `git diff 789dbf3cd -- 
plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java` — it MUST 
print nothing (file identical to `main`).
+
+- [ ] **Step 3: Remove the `<bean-selection>` from `struts-plugin.xml`**
+
+In `plugins/json/src/main/resources/struts-plugin.xml`, delete this line (line 
68):
+
+```xml
+    <bean-selection name="jsonBeans" 
class="org.apache.struts2.json.JSONBeanSelectionProvider"/>
+```
+
+Leave everything else (the `<bean>` definitions, constants, `json-default` 
package) unchanged. The closing `</struts>` remains.
+
+- [ ] **Step 4: Create `struts-deferred.xml` with the moved element**
+
+Create `plugins/json/src/main/resources/struts-deferred.xml` (mirrors 
`plugins/velocity/src/main/resources/struts-deferred.xml`):
+
+```xml
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+        "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+        "https://struts.apache.org/dtds/struts-6.0.dtd";>
+
+<struts>
+
+    <bean-selection name="jsonBeans" 
class="org.apache.struts2.json.JSONBeanSelectionProvider"/>
+
+</struts>
+```
+
+- [ ] **Step 5: Create the marker writer fixture**
+
+Create 
`plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java`:
+
+```java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+package org.apache.struts2.json;
+
+import java.util.Collection;
+import java.util.regex.Pattern;
+
+/**
+ * Marker writer proving that a user-configured {@code struts.json.writer} 
override
+ * wins over the default {@link StrutsJSONWriter}. Emits a sentinel so 
output-level
+ * assertions are unambiguous.
+ */
+public class CustomTestJSONWriter implements JSONWriter {
+
+    public static final String SENTINEL = "{\"__customWriter__\":true}";
+
+    @Override
+    public String write(Object object) throws JSONException {
+        return SENTINEL;
+    }
+
+    @Override
+    public String write(Object object, Collection<Pattern> excludeProperties,
+                        Collection<Pattern> includeProperties,
+                        boolean excludeNullProperties) throws JSONException {
+        return SENTINEL;
+    }
+
+    // Configuration setters are intentionally ignored: this marker always 
emits SENTINEL.
+    // (in-body comment required by Sonar java:S1186 — empty methods must be 
explained)
+    @Override public void setIgnoreHierarchy(boolean ignoreHierarchy) { /* 
no-op marker */ }
+    @Override public void setEnumAsBean(boolean enumAsBean) { /* no-op marker 
*/ }
+    @Override public void setDateFormatter(String defaultDateFormat) { /* 
no-op marker */ }
+    @Override public void setCacheBeanInfo(boolean cacheBeanInfo) { /* no-op 
marker */ }
+    @Override public void setExcludeProxyProperties(boolean 
excludeProxyProperties) { /* no-op marker */ }
+}
+```
+
+- [ ] **Step 6: Create the marker reader fixture**
+
+Create 
`plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java`:
+
+```java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+package org.apache.struts2.json;
+
+/**
+ * Marker reader proving that a user-configured {@code struts.json.reader} 
override
+ * wins over the default {@link StrutsJSONReader}.
+ */
+public class CustomTestJSONReader implements JSONReader {
+
+    public static final String SENTINEL = "__customReader__";
+
+    @Override
+    public Object read(String string) throws JSONException {
+        return SENTINEL;
+    }
+
+    // Limit setters are intentionally ignored: this marker always returns 
SENTINEL.
+    // (in-body comment required by Sonar java:S1186 — empty methods must be 
explained)
+    @Override public void setMaxElements(int maxElements) { /* no-op marker */ 
}
+    @Override public void setMaxDepth(int maxDepth) { /* no-op marker */ }
+    @Override public void setMaxStringLength(int maxStringLength) { /* no-op 
marker */ }
+    @Override public void setMaxKeyLength(int maxKeyLength) { /* no-op marker 
*/ }
+}
+```
+
+- [ ] **Step 7: Create the application-style override config**
+
+Create `plugins/json/src/test/resources/struts-json-override.xml`. The ASF 
license header is
+required — Apache RAT (bound to `prepare-package`) flags any 
`src/test/resources/**/*.xml`
+without it and fails the build:
+
+```xml
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+    "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+    "https://struts.apache.org/dtds/struts-6.0.dtd";>
+<struts>
+    <bean type="org.apache.struts2.json.JSONWriter" name="customTestWriter"
+          class="org.apache.struts2.json.CustomTestJSONWriter" 
scope="prototype"/>
+    <bean type="org.apache.struts2.json.JSONReader" name="customTestReader"
+          class="org.apache.struts2.json.CustomTestJSONReader" 
scope="prototype"/>
+    <constant name="struts.json.writer" value="customTestWriter"/>
+    <constant name="struts.json.reader" value="customTestReader"/>
+</struts>
+```
+
+- [ ] **Step 8: Create the regression test**
+
+Create 
`plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java`.
 It boots the real `Dispatcher` chain; `struts-deferred.xml` is auto-loaded 
last by `Dispatcher.init()` (do NOT list it in `config`). Asserts both the 
default container binding and the effective use:
+
+```java
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+package org.apache.struts2.json;
+
+import org.apache.struts2.junit.StrutsTestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Regression test for WW-5641: {@code struts.json.writer} / {@code 
struts.json.reader}
+ * overrides from an application config were ignored on the 7.2.x line because 
the JSON
+ * plugin's {@code <bean-selection>} ran at plugin-parse time, before the app 
config.
+ * Moving it to {@code struts-deferred.xml} makes it run last, so the override 
wins.
+ *
+ * <p>Boots the real Dispatcher chain. {@code struts-deferred.xml} is loaded 
unconditionally
+ * by {@code Dispatcher.init()} after the {@code config} chain, so it is 
intentionally omitted
+ * from the config init param below.</p>
+ */
+public class JSONWriterOverrideTest extends StrutsTestCase {
+
+    @Override
+    protected void setupBeforeInitDispatcher() throws Exception {
+        Map<String, String> params = new HashMap<>();
+        params.put("config", 
"struts-default.xml,struts-plugin.xml,struts-json-override.xml");
+        dispatcherInitParams = params;
+    }
+
+    /**
+     * The default JSONWriter binding must resolve to the app override, not 
StrutsJSONWriter.
+     */
+    public void testCustomWriterWinsAsDefaultBinding() {
+        JSONWriter writer = container.getInstance(JSONWriter.class);
+        assertEquals("struts.json.writer override was ignored; default 
StrutsJSONWriter was used",
+                CustomTestJSONWriter.class, writer.getClass());
+    }
+
+    /**
+     * The default JSONReader binding must resolve to the app override, not 
StrutsJSONReader.
+     */
+    public void testCustomReaderWinsAsDefaultBinding() {
+        JSONReader reader = container.getInstance(JSONReader.class);
+        assertEquals("struts.json.reader override was ignored; default 
StrutsJSONReader was used",
+                CustomTestJSONReader.class, reader.getClass());
+    }
+
+    /**
+     * End-to-end: the writer JSONUtil actually serializes with must be the 
override.
+     */
+    public void testJSONUtilUsesCustomWriter() throws Exception {
+        JSONUtil jsonUtil = container.getInstance(JSONUtil.class);
+        assertEquals(CustomTestJSONWriter.SENTINEL, jsonUtil.serialize(new 
Object(), false));
+    }
+
+    /**
+     * End-to-end: the reader JSONUtil exposes must be the override.
+     */
+    public void testJSONUtilUsesCustomReader() {
+        JSONUtil jsonUtil = container.getInstance(JSONUtil.class);
+        assertEquals(CustomTestJSONReader.class, 
jsonUtil.getReader().getClass());
+    }
+}
+```
+
+- [ ] **Step 9: Verify the test reproduces the bug BEFORE the resource move**
+
+To confirm the test is a real reproduction, temporarily stash only the two 
resource changes and run the test against the old wiring:
+
+```bash
+git stash push -- plugins/json/src/main/resources/struts-plugin.xml 
plugins/json/src/main/resources/struts-deferred.xml
+mvn test -DskipAssembly -pl plugins/json -Dtest=JSONWriterOverrideTest
+git stash pop
+```
+
+Expected with the resources stashed (old wiring, `<bean-selection>` still in 
`struts-plugin.xml`, no deferred file): all four tests FAIL — default bindings 
resolve to `StrutsJSONWriter`/`StrutsJSONReader` and `serialize` returns `{}`. 
If any test passes here, the bootstrap is not reproducing the ordering — stop 
and re-check the `config` param before proceeding. (The `JSONUtil` revert from 
Steps 1-2 stays in place during this check; it is not what causes the bug.)
+
+- [ ] **Step 10: Run the test to verify it PASSES with the fix in place**
+
+Run: `mvn test -DskipAssembly -pl plugins/json -Dtest=JSONWriterOverrideTest`
+Expected: all four tests PASS. Output pristine — no "default mapping already 
assigned" warning for `JSONWriter`/`JSONReader`.
+
+- [ ] **Step 11: Run the full JSON plugin suite AND the license check**
+
+Run: `mvn test -DskipAssembly -pl plugins/json`
+Expected: BUILD SUCCESS, all tests pass (DoS-limit, bean-info caching, and the 
`new JSONUtil()` + `setWriter`/`setReader` tests are all unaffected).
+
+Then run the Apache RAT license check — it binds to `prepare-package`, so `mvn 
test` does NOT
+exercise it, and a new resource file missing the ASF header will pass tests 
but fail CI/`install`:
+
+Run: `mvn apache-rat:check -pl plugins/json`
+Expected: BUILD SUCCESS (exit 0), no `UNAPPROVED` files. If it flags 
`struts-json-override.xml`,
+the ASF header from Step 7 is missing — add it and re-run.
+
+- [ ] **Step 12: Commit**
+
+```bash
+git add plugins/json/src/main/java/org/apache/struts2/json/JSONUtil.java \
+        plugins/json/src/main/resources/struts-plugin.xml \
+        plugins/json/src/main/resources/struts-deferred.xml \
+        
plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java \
+        
plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java \
+        
plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java \
+        plugins/json/src/test/resources/struts-json-override.xml
+git commit -m "WW-5641 fix: run JSON bean-selection from struts-deferred.xml
+
+The JSON plugin declared <bean-selection> in struts-plugin.xml, which runs
+at plugin-parse time, before the application struts.xml is folded in. That
+froze the JSONWriter/JSONReader default binding to StrutsJSONWriter/Reader,
+so struts.json.writer / struts.json.reader overrides were ignored.
+
+Move the element to struts-deferred.xml, which Dispatcher loads last (after
+the app config and core's StrutsBeanSelectionProvider), so the alias honors
+the override. Mirrors the velocity plugin. JSONUtil is unchanged from main.
+
+Co-Authored-By: Claude Opus 4.8 <[email protected]>"
+```
+
+---
+
+## Self-Review
+
+**Spec coverage:**
+- Move `<bean-selection>` to `struts-deferred.xml` → Steps 3-4. ✓
+- Revert `JSONUtil` to `main` state → Steps 1-2 + git-diff verification. ✓
+- Keep `<bean>` defs/constants in `struts-plugin.xml` (velocity mirror) → Step 
3 leaves them; Global Constraints. ✓
+- No core / `JSONBeanSelectionProvider` / DoS / `StrutsJSONWriter` changes → 
no task touches them; Global Constraints forbids it. ✓
+- Regression test asserts BOTH default binding AND effective use, for writer 
AND reader → Step 8 (4 tests). ✓
+- `StrutsTestCase` real Dispatcher; `struts-deferred.xml` not in `config` 
param → Step 8 + Global Constraints + Step 10 pristine-output check. ✓
+- FAIL-before / PASS-after → Steps 9 and 10. ✓
+- Full module suite green → Step 11. ✓
+
+**Placeholder scan:** No TBD/TODO; every code/resource step shows complete 
content. ✓
+
+**Type consistency:** Fixture method signatures match the 
`JSONWriter`/`JSONReader` interfaces. `serialize(Object, boolean)` and 
`getReader()` exist on `JSONUtil`. `container.getInstance(Class)` used for 
default-binding assertions. Bean names (`customTestWriter`/`customTestReader`) 
match between the override XML and the constant values. The deferred element's 
`name`/`class` match the line removed from `struts-plugin.xml`. ✓
diff --git 
a/docs/superpowers/specs/2026-07-06-WW-5641-json-writer-reader-override-regression-design.md
 
b/docs/superpowers/specs/2026-07-06-WW-5641-json-writer-reader-override-regression-design.md
new file mode 100644
index 000000000..5904eecdf
--- /dev/null
+++ 
b/docs/superpowers/specs/2026-07-06-WW-5641-json-writer-reader-override-regression-design.md
@@ -0,0 +1,161 @@
+# WW-5641: Restore `struts.json.writer` / `struts.json.reader` override
+
+**Ticket:** [WW-5641](https://issues.apache.org/jira/browse/WW-5641)
+**Type:** Bug (regression)
+**Component:** Plugin - JSON
+**Date:** 2026-07-06
+
+## Problem
+
+A custom JSON writer/reader configured the documented way is silently ignored 
on the 7.2.x
+line. The framework always uses the default `StrutsJSONWriter` / 
`StrutsJSONReader` regardless
+of the application's override:
+
+```xml
+<bean type="org.apache.struts2.json.JSONWriter" name="flexJSONWriter"
+      class="org.demo.FlexJSONWriter" scope="prototype"/>
+<constant name="struts.json.writer" value="flexJSONWriter"/>
+```
+
+This worked on 7.1.x and regressed in 7.2.x. The extension point is still 
documented at
+<https://struts.apache.org/plugins/json/>, so this is a regression, not an 
intended API change.
+Confirmed by the `json-customize` module reproduction in 
apache/struts-examples PR #535
+(`ProduceActionTest`): passes on 7.1.x, fails on 7.2.x.
+
+This is a **functional regression** restoring a documented extension point — 
not a security fix
+(no OGNL injection, parameter-filtering bypass, auth bypass, RCE, SSRF, path 
traversal, etc.),
+so it follows the normal PR path.
+
+## Root cause (verified against current `main`)
+
+The 7.2.x JSON hardening rework added a `<bean-selection name="jsonBeans" 
.../>` element to the
+plugin's **`struts-plugin.xml`** to select the `JSONWriter` / `JSONReader` 
default binding via
+`JSONBeanSelectionProvider` → `AbstractBeanSelectionProvider.alias()`. The 
defect is *where*
+that element lives, which determines *when* it runs.
+
+- `XmlDocConfigurationProvider.registerBeanSelection()` (`:215-222`) invokes 
the bean-selection
+  provider **inline, the moment the `<bean-selection>` element is parsed** 
inside the JSON
+  plugin's `struts-plugin.xml`.
+- In `Dispatcher.init()` the config sources are registered in this order (see
+  `Dispatcher.java:696-704`): `struts-default.xml` → all `struts-plugin.xml` 
(plugin discovery)
+  → the app's `struts.xml` (`init_TraditionalXmlConfigurations`) → core's
+  `StrutsBeanSelectionProvider` (`init_AliasStandardObjects`) → 
`struts-deferred.xml`
+  (`init_DeferredXmlConfigurations`).
+- So the JSON `<bean-selection>` fires while parsing `struts-plugin.xml`, 
**before** the app's
+  `struts.xml` folds its `flexJSONWriter` bean and its `struts.json.writer` 
constant into the
+  shared `props`/builder. `alias()` reads 
`props.getProperty("struts.json.writer", "struts")` =
+  `"struts"` (the plugin default) and locks `JSONWriter/DEFAULT_NAME → 
StrutsJSONWriter`.
+  `alias()` binds the default name only once (guarded by 
`!builder.contains(type, DEFAULT_NAME)`),
+  so nothing re-selects when the app config loads afterward. `JSONUtil` 
injects that frozen
+  default binding and never sees the override.
+
+The 7.1.x code was immune because it resolved the writer/reader by the 
constant value at
+container-use time — after the full container (including the app `struts.xml`) 
was built.
+
+## Chosen approach: run the JSON `<bean-selection>` late, via 
`struts-deferred.xml`
+
+The framework already provides the idiomatic, order-safe mechanism for exactly 
this. From
+`Dispatcher.java`:
+
+> `struts-deferred.xml` can be used to load configuration which is sensitive 
to loading order
+> such as 'bean-selection' elements
+
+`init_DeferredXmlConfigurations()` loads `struts-deferred.xml` **last** — 
after the app's
+`struts.xml` and after core's `StrutsBeanSelectionProvider`. A 
`<bean-selection>` placed there
+runs once the app's beans and constants are already in `props`/builder, so 
`alias()` reads the
+*effective* `struts.json.writer` value and aliases `JSONWriter/DEFAULT_NAME` 
to the application's
+override.
+
+This is the established convention in-tree: the **velocity plugin** keeps its 
`<bean>`
+definitions in `struts-plugin.xml` and places its `<bean-selection 
name="velocityBeans" .../>`
+in `plugins/velocity/src/main/resources/struts-deferred.xml`. The JSON plugin 
is the outlier
+that put `<bean-selection>` in `struts-plugin.xml`. Core's own 
`StrutsBeanSelectionProvider` uses
+the same late-running principle (registered by `init_AliasStandardObjects`, 
after all XML),
+which is why application overrides of core beans (`struts.objectFactory`, 
converters, …) work.
+
+### Changes
+
+1. **Remove** the `<bean-selection name="jsonBeans" .../>` element from
+   `plugins/json/src/main/resources/struts-plugin.xml` (line 68). Leave the 
`<bean name="struts">`
+   writer/reader definitions, the `JSONUtil`/`JSONCacheDestroyable` beans, the 
constants, and the
+   `json-default` package exactly as they are — mirroring velocity.
+2. **Create** `plugins/json/src/main/resources/struts-deferred.xml` containing 
the moved
+   `<bean-selection name="jsonBeans" 
class="org.apache.struts2.json.JSONBeanSelectionProvider"/>`.
+3. **Revert `JSONUtil` to the 7.2.x state**: restore `@Inject` on 
`setWriter(JSONWriter)` and
+   `setReader(JSONReader)`, and remove the `setContainer(Container)` method 
and the
+   `org.apache.struts2.inject.Container` import introduced by the earlier 
(rejected) attempt.
+   No Java source change remains — `JSONUtil` again injects the (now correct) 
default binding.
+
+### Why this over resolving by-name inside `JSONUtil`
+
+An alternative (rejected) was to have `JSONUtil` inject the `Container` and 
resolve the
+writer/reader by the constant value at runtime. That works but sidesteps the 
framework's standard
+bean-override mechanism, leaves the default `JSONWriter`/`JSONReader` 
container binding pointing
+at the wrong implementation, and only fixes `JSONUtil`'s own consumption. The 
deferred
+`<bean-selection>` fixes the binding itself: 
`container.getInstance(JSONWriter.class)` returns the
+override for *any* consumer, `JSONUtil` needs no change, and the plugin now 
matches velocity and
+core convention.
+
+### What is intentionally *not* changed
+
+- `StrutsJSONWriter` / `StrutsJSONReader` remain the shipped defaults (they 
are the `name="struts"`
+  beans, aliased to the default only when no app override is present).
+- The JSON DoS limits (`struts.json.maxDepth`, `maxElements`, `maxLength`, 
`maxStringLength`,
+  `maxKeyLength`) and their `@Inject` setters are untouched.
+- No core class changes. `JSONBeanSelectionProvider` and 
`AbstractBeanSelectionProvider` are
+  unchanged — only *when* the provider runs changes, driven by which config 
file carries the
+  element.
+
+### Thread-safety
+
+`JSONUtil` remains `prototype`; the writer/reader remain `prototype` (aliased 
with
+`Scope.PROTOTYPE`). Lifecycle is identical to current `main` — no 
thread-safety change for the
+stateful bean-info caching (`StrutsJSONWriter.clearBeanInfoCaches()`, 
`JSONCacheDestroyable`).
+
+## Test plan
+
+New files under `plugins/json/src/test/...`:
+
+1. **`CustomTestJSONWriter`** — implements the current `JSONWriter` interface; 
`write(...)`
+   returns a sentinel (`{"__customWriter__":true}`) for unambiguous 
serialization assertions.
+   Implements: `write(Object)`, `write(Object, Collection<Pattern>, 
Collection<Pattern>, boolean)`,
+   `setIgnoreHierarchy`, `setEnumAsBean`, `setDateFormatter`, 
`setCacheBeanInfo`,
+   `setExcludeProxyProperties`.
+2. **`CustomTestJSONReader`** — implements the current `JSONReader` interface 
as a marker:
+   `read(String)` returns a sentinel, 
`setMaxElements`/`setMaxDepth`/`setMaxStringLength`/
+   `setMaxKeyLength` are no-ops.
+3. **Override config** `src/test/resources/struts-json-override.xml` — 
registers both custom
+   beans (`scope="prototype"`) and the `struts.json.writer` / 
`struts.json.reader` constants
+   pointing at them.
+4. **`JSONWriterOverrideTest extends StrutsTestCase`** — boots the real 
`Dispatcher` config
+   chain so the deferred `struts-deferred.xml` runs after the override. The 
config init param is
+   `struts-default.xml,struts-plugin.xml,struts-json-override.xml` (defaults → 
JSON plugin beans
+   → app override). `struts-deferred.xml` is **not** listed: 
`Dispatcher.init()` always runs
+   `init_DeferredXmlConfigurations()` after the `config` chain, so the JSON 
plugin's
+   `struts-deferred.xml` (on the classpath) loads last on its own — listing it 
explicitly would
+   load it twice and emit a spurious "default mapping already assigned" 
warning. Because the fix
+   now corrects the binding itself, the test asserts at **both** levels:
+   - **Default container binding:** `container.getInstance(JSONWriter.class)` 
is a
+     `CustomTestJSONWriter`; `container.getInstance(JSONReader.class)` is a 
`CustomTestJSONReader`.
+   - **Effective use:** `jsonUtil.serialize(someBean, false)` returns the 
writer sentinel and
+     `jsonUtil.getReader().getClass()` is `CustomTestJSONReader`.
+
+   > Note: `XWorkTestCase` alone uses only 
`StrutsDefaultConfigurationProvider` and loads neither
+   > the plugin `struts-plugin.xml` nor `struts-deferred.xml`, so it cannot 
reproduce the ordering.
+   > `StrutsTestCase` (full `Dispatcher` init) is required.
+
+Both assertions must **FAIL on current `main`** (before moving the element) 
and **PASS after**.
+
+## Verification
+
+- New `JSONWriterOverrideTest` fails before the fix, passes after.
+- Full `struts2-json-plugin` module suite green: `mvn test -DskipAssembly -pl 
plugins/json` —
+  DoS-limit and caching tests unaffected.
+
+## Scope guardrails
+
+- No change to `StrutsJSONWriter` / `StrutsJSONReader`, the DoS constants, 
`JSONUtil` (net-zero
+  after revert), `JSONBeanSelectionProvider`, or any core class.
+- Change is confined to two plugin resource files (`struts-plugin.xml`, new 
`struts-deferred.xml`)
+  plus new test fixtures.
+- Delivered via a PR from a feature branch referencing WW-5641 (never pushed 
to `main`).
diff --git a/plugins/json/src/main/resources/struts-deferred.xml 
b/plugins/json/src/main/resources/struts-deferred.xml
new file mode 100644
index 000000000..b9e8bd9a6
--- /dev/null
+++ b/plugins/json/src/main/resources/struts-deferred.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+        "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+        "https://struts.apache.org/dtds/struts-6.0.dtd";>
+
+<struts>
+
+    <bean-selection name="jsonBeans" 
class="org.apache.struts2.json.JSONBeanSelectionProvider"/>
+
+</struts>
diff --git a/plugins/json/src/main/resources/struts-plugin.xml 
b/plugins/json/src/main/resources/struts-plugin.xml
index 3d246ff36..2cb27dcae 100644
--- a/plugins/json/src/main/resources/struts-plugin.xml
+++ b/plugins/json/src/main/resources/struts-plugin.xml
@@ -64,6 +64,4 @@
         </interceptors>
 
     </package>
-
-    <bean-selection name="jsonBeans" 
class="org.apache.struts2.json.JSONBeanSelectionProvider"/>
 </struts>
diff --git 
a/plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java 
b/plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java
new file mode 100644
index 000000000..d39a166de
--- /dev/null
+++ 
b/plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONReader.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+package org.apache.struts2.json;
+
+/**
+ * Marker reader proving that a user-configured {@code struts.json.reader} 
override
+ * wins over the default {@link StrutsJSONReader}.
+ */
+public class CustomTestJSONReader implements JSONReader {
+
+    public static final String SENTINEL = "__customReader__";
+
+    @Override
+    public Object read(String string) throws JSONException {
+        return SENTINEL;
+    }
+
+    // Limit setters are intentionally ignored: this marker always returns 
SENTINEL.
+    @Override public void setMaxElements(int maxElements) { /* no-op marker */ 
}
+    @Override public void setMaxDepth(int maxDepth) { /* no-op marker */ }
+    @Override public void setMaxStringLength(int maxStringLength) { /* no-op 
marker */ }
+    @Override public void setMaxKeyLength(int maxKeyLength) { /* no-op marker 
*/ }
+}
diff --git 
a/plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java 
b/plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java
new file mode 100644
index 000000000..d201b3bd4
--- /dev/null
+++ 
b/plugins/json/src/test/java/org/apache/struts2/json/CustomTestJSONWriter.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+package org.apache.struts2.json;
+
+import java.util.Collection;
+import java.util.regex.Pattern;
+
+/**
+ * Marker writer proving that a user-configured {@code struts.json.writer} 
override
+ * wins over the default {@link StrutsJSONWriter}. Emits a sentinel so 
output-level
+ * assertions are unambiguous.
+ */
+public class CustomTestJSONWriter implements JSONWriter {
+
+    public static final String SENTINEL = "{\"__customWriter__\":true}";
+
+    @Override
+    public String write(Object object) throws JSONException {
+        return SENTINEL;
+    }
+
+    @Override
+    public String write(Object object, Collection<Pattern> excludeProperties,
+                        Collection<Pattern> includeProperties,
+                        boolean excludeNullProperties) throws JSONException {
+        return SENTINEL;
+    }
+
+    // Configuration setters are intentionally ignored: this marker always 
emits SENTINEL.
+    @Override public void setIgnoreHierarchy(boolean ignoreHierarchy) { /* 
no-op marker */ }
+    @Override public void setEnumAsBean(boolean enumAsBean) { /* no-op marker 
*/ }
+    @Override public void setDateFormatter(String defaultDateFormat) { /* 
no-op marker */ }
+    @Override public void setCacheBeanInfo(boolean cacheBeanInfo) { /* no-op 
marker */ }
+    @Override public void setExcludeProxyProperties(boolean 
excludeProxyProperties) { /* no-op marker */ }
+}
diff --git 
a/plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java
 
b/plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java
new file mode 100644
index 000000000..155bf793b
--- /dev/null
+++ 
b/plugins/json/src/test/java/org/apache/struts2/json/JSONWriterOverrideTest.java
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+package org.apache.struts2.json;
+
+import org.apache.struts2.junit.StrutsTestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Regression test for WW-5641: {@code struts.json.writer} / {@code 
struts.json.reader}
+ * overrides from an application config were ignored on the 7.2.x line because 
the JSON
+ * plugin's {@code <bean-selection>} ran at plugin-parse time, before the app 
config.
+ * Moving it to {@code struts-deferred.xml} makes it run last, so the override 
wins.
+ *
+ * <p>Boots the real Dispatcher chain. {@code struts-deferred.xml} is loaded 
unconditionally
+ * by {@code Dispatcher.init()} after the {@code config} chain, so it is 
intentionally omitted
+ * from the config init param below.</p>
+ */
+public class JSONWriterOverrideTest extends StrutsTestCase {
+
+    @Override
+    protected void setupBeforeInitDispatcher() throws Exception {
+        Map<String, String> params = new HashMap<>();
+        params.put("config", 
"struts-default.xml,struts-plugin.xml,struts-json-override.xml");
+        dispatcherInitParams = params;
+    }
+
+    /**
+     * The default JSONWriter binding must resolve to the app override, not 
StrutsJSONWriter.
+     */
+    public void testCustomWriterWinsAsDefaultBinding() {
+        JSONWriter writer = container.getInstance(JSONWriter.class);
+        assertEquals("struts.json.writer override was ignored; default 
StrutsJSONWriter was used",
+                CustomTestJSONWriter.class, writer.getClass());
+    }
+
+    /**
+     * The default JSONReader binding must resolve to the app override, not 
StrutsJSONReader.
+     */
+    public void testCustomReaderWinsAsDefaultBinding() {
+        JSONReader reader = container.getInstance(JSONReader.class);
+        assertEquals("struts.json.reader override was ignored; default 
StrutsJSONReader was used",
+                CustomTestJSONReader.class, reader.getClass());
+    }
+
+    /**
+     * End-to-end: the writer JSONUtil actually serializes with must be the 
override.
+     */
+    public void testJSONUtilUsesCustomWriter() throws Exception {
+        JSONUtil jsonUtil = container.getInstance(JSONUtil.class);
+        assertEquals(CustomTestJSONWriter.SENTINEL, jsonUtil.serialize(new 
Object(), false));
+    }
+
+    /**
+     * End-to-end: the reader JSONUtil exposes must be the override.
+     */
+    public void testJSONUtilUsesCustomReader() {
+        JSONUtil jsonUtil = container.getInstance(JSONUtil.class);
+        assertEquals(CustomTestJSONReader.class, 
jsonUtil.getReader().getClass());
+    }
+}
diff --git a/plugins/json/src/test/resources/struts-json-override.xml 
b/plugins/json/src/test/resources/struts-json-override.xml
new file mode 100644
index 000000000..097b16484
--- /dev/null
+++ b/plugins/json/src/test/resources/struts-json-override.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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
+ *
+ *  http://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.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+    "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+    "https://struts.apache.org/dtds/struts-6.0.dtd";>
+<struts>
+    <bean type="org.apache.struts2.json.JSONWriter" name="customTestWriter"
+          class="org.apache.struts2.json.CustomTestJSONWriter" 
scope="prototype"/>
+    <bean type="org.apache.struts2.json.JSONReader" name="customTestReader"
+          class="org.apache.struts2.json.CustomTestJSONReader" 
scope="prototype"/>
+    <constant name="struts.json.writer" value="customTestWriter"/>
+    <constant name="struts.json.reader" value="customTestReader"/>
+</struts>


Reply via email to