jdaugherty commented on PR #16414:
URL: https://github.com/apache/grails-core/pull/16414#issuecomment-5962785988

   I asked AI to summarize some of the differences to help this discussion, 
it's included below (it was done against the 7.x branch, but I think this 
comparison brings to light several of the problems): 
   
   # Grails JSON: OpenJSON versus Jackson
   
   **Jackson is the stronger long-term option, but neither replacement 
preserves Grails behavior automatically.** OpenJSON is closer to the original 
API; Jackson offers more control over parsing, numeric precision, and resource 
limits.
   
   I compared the Grails `7.0.x` source and ran parsing probes against **Grails 
7.0.4, OpenJSON 1.0.13, Jackson 2.20.0, and Jackson 3.0.0**. Jackson results 
below use untyped `Object`/`Map`/`List` deserialization, not POJO binding. 
These are compatibility findings for those versions, not a test of the 
particular proposal.
   
   **The syntax differences are substantial.** Grails deliberately accepts 
several nonstandard forms, documented in its API. Jackson rejects most by 
default, with optional features for selected extensions. Sources: [Grails 
JSONObject API][grails-object] and [Jackson JsonReadFeature API][jackson-read].
   
   | Input or behavior | Grails fork | OpenJSON | Jackson defaults |
   |---|---|---|---|
   | Single quotes: `{'x':'a'}` | Accepts | Accepts | Rejects; configurable |
   | Comments: `//`, `/* */`, `#` | Accepts | Accepts | Rejects; configurable |
   | Unquoted string value: `{x:hello}` | Accepts | Accepts | Rejects; allowing 
unquoted **field names** does not permit bare string values |
   | Uppercase literals: `{"x":TRUE}` | Boolean `true` | Boolean `true` | 
Rejects |
   | Alternate separators: `{x=>1;y:2}` | Accepts | Accepts | Rejects |
   | Object trailing comma: `{"x":1,}` | Accepts | **Rejects** | Rejects; 
configurable |
   | Array trailing comma: `[1,]` | `[1]` | **`[1,null]`** | Rejects; 
configurable |
   | Missing array element: `[1,,2]` | `[1,null,2]` | `[1,null,2]` | Rejects; 
configurable |
   | Octal number: `{"x":010}` | Integer `8` | Integer `8` | Rejects; 
permitting leading zeros interprets this as decimal `10` |
   | Numeric object key: `{12:3}` | Key becomes `"12"` | Rejects | Rejects by 
default |
   | Hex string escape: `{"x":"\x41"}` | String `"A"` | **String `"x41"`** | 
Rejects |
   | Literal newline inside a quoted string | Rejects | Accepts | Rejects; 
configurable |
   | `{"x":new Date(0)}` | Java `Date` | Rejects | Rejects |
   | Duplicate keys: `{"x":1,"x":2}` | Last value wins | Last value wins | Last 
value wins; rejection can be enabled |
   | Valid object followed by garbage | Ignores suffix | Ignores suffix | 
Jackson 2 ignores it by default; **Jackson 3 rejects it** |
   
   The trailing-content difference is a documented Jackson 3 default change: 
`FAIL_ON_TRAILING_TOKENS` became enabled. Source: [Jackson 3 migration 
guide][jackson-migration].
   
   That exposes a weakness in the “OpenJSON is a compatible replacement” 
argument: it can both reject previously accepted input **and silently change 
the resulting data**.
   
   **Numeric handling is probably the most consequential difference—even for 
valid JSON.**
   
   These are results from the probes:
   
   | Numeric token | Grails | OpenJSON | Jackson, untyped defaults |
   |---|---|---|---|
   | `42` | `Integer` | `Integer` | `Integer` |
   | `9223372036854775808` | Exact `BigInteger` | Rounded `Double` | Exact 
`BigInteger` |
   | `0.123456789012345678901` | Exact `BigDecimal` | Rounded `Double` | 
Rounded `Double` |
   | `1.00` | `BigDecimal`, scale 2 | `Double` `1.0` | `Double` `1.0` |
   | `1e3` | `BigDecimal` | `Double` | `Double` |
   
   Grails uses a custom selection rule: integral numbers generally become 
`Integer`, `Long`, or `BigInteger`; other numbers become `Double` only when its 
`BigDecimal` comparison succeeds, otherwise they remain `BigDecimal`. 
Consequently, **scale and notation can affect the Java type**.
   
   Jackson can preserve decimal precision with `USE_BIG_DECIMAL_FOR_FLOATS`, 
but that makes *all* untyped floating-point values `BigDecimal`. It does not 
reproduce Grails’ mixed `Double`/`BigDecimal` rule. Exact compatibility would 
need custom numeric conversion, ideally using the original token text. Source: 
[Jackson DeserializationFeature API][jackson-deser].
   
   OpenJSON’s parser instead tries `Integer`, `Long`, then `Double`. Moving to 
it without adaptation would undo Grails’ large-number preservation. Source: 
[OpenJSON JSONTokener source][openjson-tokener].
   
   **The returned object model is a separate compatibility problem.**
   
   - **Grails:** `JSONObject` implements `Map`; `JSONArray` implements `List`. 
Parsed JSON null becomes Java `null`.
   - **OpenJSON:** similarly named containers, but they do not implement those 
collection interfaces. Parsed null uses `JSONObject.NULL`.
   - **Jackson:** untyped binding gives ordinary maps/lists and Java `null`; 
tree parsing gives `ObjectNode`, `ArrayNode`, and `NullNode`.
   
   Grails’ collection interfaces are part of its public API. Replacing them 
affects casts, collection operations, nested access, and callers expecting the 
Grails classes—not merely imports. Sources: [Grails JSONObject 
API][grails-object] and [Grails JSONArray API][grails-array].
   
   My assessment of the tradeoffs:
   
   | Choice | Pros | Cons |
   |---|---|---|
   | **Keep the Grails fork** | Best preservation of existing syntax, numeric 
rules, and public types | Grails continues maintaining parser code, quirks, and 
defensive limits |
   | **OpenJSON** | Small standalone dependency; familiar API; Apache-licensed 
clean-room implementation | Precision regressions; subtle syntax changes; null 
sentinel and collection-interface differences still require adaptation |
   | **Jackson** | Streaming support; configurable syntax, duplicate handling, 
precision, and processing limits; broader integration opportunities | Greater 
migration scope; defaults differ; cannot reproduce every legacy extension 
through feature switches |
   
   OpenJSON explicitly describes itself as an Android-derived clean-room 
implementation, rather than the same source Grails forked. Its inspected parser 
also has a default nesting limit of 100. Jackson provides configurable limits 
for nesting and number/string lengths through `StreamReadConstraints`. These 
limits themselves need compatibility testing. Sources: [OpenJSON 
project][openjson-project], [OpenJSON JSONTokener source][openjson-tokener], 
and [Jackson processing-limit documentation][jackson-limits].
   
   I would not assume a performance winner without measuring the actual Grails 
path. Jackson’s streaming advantage can disappear if the implementation 
constructs a Jackson tree and then copies it into Grails containers.
   
   **My recommendation is Jackson behind a Grails compatibility facade**, with 
explicit decisions about which legacy behaviors to retain.
   
   An alternative framing is to separate two proposals:
   
   1. **Replace the parsing engine:** preserve Grails’ public containers, null 
handling, numeric contract, and exception behavior.
   2. **Modernize the JSON contract:** deliberately reject legacy syntax or 
change returned types in a documented breaking release.
   
   That separation makes the proposal much easier to evaluate. Switching 
parsers should not implicitly switch Grails databinding or JSON marshalling to 
Jackson POJO behavior.
   
   Before accepting either replacement, I’d require a differential test suite 
covering the examples above, nested containers, missing versus explicit null, 
numeric precision and scale, errors, and size/depth limits. **OpenJSON needs 
that suite just as much as Jackson does; API resemblance is insufficient 
evidence of compatibility.**
   
   [grails-object]: 
https://grails.apache.org/docs/7.0.4/api/org/grails/web/json/JSONObject.html
   [grails-array]: 
https://grails.apache.org/docs/latest/api/org/grails/web/json/JSONArray.html
   [jackson-read]: 
https://javadoc.io/static/com.fasterxml.jackson.core/jackson-core/2.16.2/com/fasterxml/jackson/core/json/JsonReadFeature.html
   [jackson-migration]: 
https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md
   [jackson-deser]: 
https://fasterxml.github.io/jackson-databind/javadoc/2.14/com/fasterxml/jackson/databind/DeserializationFeature.html
   [openjson-tokener]: 
https://github.com/openjson/openjson/blob/master/src/main/java/com/github/openjson/JSONTokener.java
   [openjson-project]: https://github.com/openjson/openjson
   [jackson-limits]: 
https://github.com/FasterXML/jackson/wiki/Jackson-Release-2.15
   


-- 
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