Skip to content

Commit 397f32d

Browse files
wnm3claude
andcommitted
Document retained Jackson 2 behaviors and known issues (#430)
- Add a "Retained Jackson 2 behaviors (as-built)" section to the design spec listing each compatibility decision (container asText(""), POJONode emptiness guards, (long) d narrowing, FAIL_ON_TRAILING_TOKENS, unchecked-exception catches), plus the pre-existing Tester EOF NPE and the acceptable transitive jackson-annotations dependency. - Add explanatory comments at the MatchFunction/ReplaceFunction guard sites so the isTextual()-guarded emptiness checks are self-documenting. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 340765f commit 397f32d

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

docs/specs/Jackson-3-Migration-Design.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,43 @@ becomes unreachable, or the type is no longer resolvable, replace with
144144
`configure(feature, boolean)` shape.
145145
- **`new ObjectMapper()` availability** — if the no-arg constructor is unavailable/discouraged in
146146
3.x, fall back to `JsonMapper.shared()` or `new JsonMapper()`.
147+
148+
## Retained Jackson 2 behaviors (as-built)
149+
150+
Jackson 3 tightened several coercion/parse defaults that would otherwise change observable
151+
behavior. Each site below was fixed to **preserve the Jackson 2 behavior** — no test assertion or
152+
expected value was changed. These are the compatibility decisions a future maintainer should know
153+
about (and can revisit if the project decides to adopt the stricter Jackson 3 semantics):
154+
155+
- **Container coercion — `ArrayUtils.compare` (main).** Jackson 3's no-arg `asText()`/`asString()`
156+
throws for container nodes (`ObjectNode`/`ArrayNode`); Jackson 2 returned `""`. Fixed by passing
157+
the default: `asText("")`. `$sort` on non-scalar operands therefore coerces to `""` exactly as
158+
before instead of throwing.
159+
- **Regex `POJONode` emptiness checks — `MatchFunction`, `ReplaceFunction` (main).** A compiled
160+
`RegularExpression` is stored in a `POJONode`. Jackson 2's `asText()` returned the POJO's
161+
`toString()` (non-empty); Jackson 3 throws for a `POJONode`. The empty-pattern checks are now
162+
guarded by `isTextual()` so the throwing call is never reached for a `POJONode`, while the
163+
boolean outcome is identical to Jackson 2.
164+
- **Out-of-range `asLong()``AgnosticTestSuite` (test harness).** Jackson 3's `DoubleNode.asLong()`
165+
throws when the value is outside `long` range (e.g. `1.0E46`); Jackson 2 performed a silent
166+
narrowing cast. Expected-value normalization now uses `(long) d`, replicating Jackson 2 exactly.
167+
- **`FAIL_ON_TRAILING_TOKENS``Utils` test mapper (test harness).** Jackson 3 flips this default
168+
to `true`; Jackson 2 silently ignored content after the first parsed value. The shared test
169+
mapper disables the feature so existing test inputs parse identically. Note: this tolerates a
170+
long-standing typo in a test input (`BasicExpressionsTests`, `"[\"h11\", \"h21\"]]"` — a stray
171+
trailing `]`). The typo was intentionally **not** corrected, to keep the test data byte-for-byte
172+
unchanged.
173+
- **Unchecked exceptions — several files.** Where Jackson 3 methods no longer throw checked
174+
`IOException`, `catch (IOException)` blocks over Jackson-only bodies became unreachable and were
175+
changed to `catch (JacksonException)`.
176+
177+
### Other known items (not changed by this migration)
178+
179+
- **Pre-existing `Tester` REPL NPE.** `Tester.main` (`Tester.java:156`) calls
180+
`expression.length()` without a null check; `JSONataUtils.prompt()` returns `null` at stdin EOF,
181+
causing a `NullPointerException` when input ends without a `q` line. This predates the migration
182+
(authored 2022) and was left as-is.
183+
- **Transitive `jackson-annotations` 2.x.** `tools.jackson.core:jackson-databind:3.2.0` still pulls
184+
in `com.fasterxml.jackson.core:jackson-annotations` — Jackson 3 continues to publish annotations
185+
under the `com.fasterxml.jackson.annotation` namespace. This is expected and acceptable; it is not
186+
a leftover Jackson 2 core/databind/dataformat dependency.

src/main/java/com/api/jsonata4java/expressions/functions/MatchFunction.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,12 @@ public JsonNode invoke(ExpressionsVisitor expressionVisitor, Function_callContex
102102
if (argString == null || !argString.isTextual() || argString.asText().isEmpty()) {
103103
throw new EvaluateRuntimeException(ERR_ARG1BADTYPE);
104104
}
105-
// Make sure that the pattern is a non-empty string
105+
// Make sure that the pattern is a non-empty string.
106+
// The emptiness check is guarded by isTextual() because a POJONode
107+
// wraps a compiled RegularExpression (inherently non-empty): under
108+
// Jackson 2 asText() returned the POJO's toString(), but Jackson 3's
109+
// asText()/asString() throws for a POJONode. Skipping the check for
110+
// non-textual nodes keeps the original behavior without that throw.
106111
if (argPattern != null && (argPattern.isTextual() || argPattern instanceof POJONode) && !(argPattern.isTextual() && argPattern.asText().isEmpty())) {
107112
RegularExpression regex = null;
108113
if (argPattern instanceof POJONode) {

src/main/java/com/api/jsonata4java/expressions/functions/ReplaceFunction.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ public JsonNode invoke(ExpressionsVisitor expressionVisitor, Function_callContex
130130
int limit = -1;
131131
// Make sure that the separator is not null
132132
if (argPattern != null && (argPattern.isTextual() || argPattern instanceof POJONode)) {
133+
// The empty-string rejection only applies to textual patterns.
134+
// A POJONode wraps a compiled RegularExpression (inherently
135+
// non-empty); under Jackson 2 asText() returned the POJO's
136+
// toString(), but Jackson 3's asText()/asString() throws for a
137+
// POJONode, so the isTextual() guard preserves the original
138+
// behavior without triggering that throw.
133139
if (argPattern.isTextual() && argPattern.asText().isEmpty()) {
134140
throw new EvaluateRuntimeException(ERR_MSG_ARG2_EMPTY_STR);
135141
}

0 commit comments

Comments
 (0)