Repository navigation
Commit c7bdbe1
authored
Let Character, CharSequence and custom deserializers see empty strings (#187)
* Let Character/CharSequence and custom deserializers see empty strings
JsonNullableJackson{2,3}Deserializer shortcut a VALUE_STRING token whose
(trimmed) text is empty into JsonNullable.undefined() whenever the
referenced type isn't String. That shortcut ran unconditionally, before
the delegate deserializer ever saw the token, which caused two issues:
- #26: JsonNullable<Character> (and CharSequence) got undefined() for ""
instead of whatever Jackson's own deserializer does with it. Broaden
the existing String-only exemption (renamed isStringDeserializer to
isStringLike) to CharSequence, its implementations (e.g. StringBuilder)
and Character.
- #46: a deserializer an application registers itself for the wrapped
type (directly or via a module) never received the empty string at
all, because this shortcut ran first regardless of what the delegate
was. Gate the shortcut on the resolved delegate being implemented by
Jackson itself, so a user-registered deserializer always sees the
token and decides for itself.
The "is this Jackson's own deserializer" check is namespace-wide
(concrete class under com.fasterxml.jackson. / tools.jackson.), not
limited to jackson-databind's own deser package. A narrower, deser-only
check would silently have changed every type handled by another
FasterXML module instead of just String-like ones: on Jackson 2,
jackson-datatype-jsr310's java.time deserializers live under
com.fasterxml.jackson.datatype.jsr310.deser; on Jackson 3, java.time
support moved inside jackson-databind itself but as a sibling of, not
nested under, its deser package (tools.jackson.databind.ext.javatime.deser,
alongside its scalars having also moved from .std to .jdk, and
BeanDeserializer from the bare deser package to .bean). Measured with
the narrower check: JsonNullable<LocalDate>/<OffsetDateTime> given ""
or " " silently turned from undefined() into a present null on both
stacks instead of throwing, which a generated OpenAPI client wrapping
every optional date field in JsonNullable would hit routinely. Added
jackson-datatype-jsr310 (version managed by the existing jackson-bom
import) as a test-scoped dependency to cover the Jackson 2 side; the
Jackson 3 side needs no extra module.
Measured on Jackson 2.22.2 and 3.2.2: Jackson's own CharacterDeserializer
does not throw for "" (yields a present null) or for a single blank
character (yields that character), so this only ever surfaces values
Jackson was already willing to produce.
Adds coverage for Character/CharSequence/StringBuilder with empty,
blank and non-blank strings, a SimpleModule-registered deserializer
now receiving the empty string on both stacks (its own test class, so
it runs once per stack instead of once per JsonProcessor), a java.time
regression class for LocalDate/OffsetDateTime, and regression guards
that Integer/Boolean/enum/plain-POJO targets still get undefined() by
default.
* Classify wrapped deserializers by the innermost delegate
isJacksonOwnDeserializer classified a deserializer by its own concrete
class name. That misclassified an application-registered
DelegatingDeserializer subclass that does nothing but forward - e.g. a
logging or validation wrapper a BeanDeserializerModifier installs
around Jackson's own BeanDeserializer - as application-owned, so
JsonNullable<Pojo> given "" started throwing InvalidFormatException
instead of returning undefined() as it does without the wrapper.
Follow JsonDeserializer#getDelegatee() / ValueDeserializer#getDelegatee()
(bounded at 16 hops so a misbehaving or cyclic wrapper cannot loop
forever) to the innermost deserializer of a delegation chain and
classify by that one. A pass-through wrapper decides nothing about
blank strings itself, so classifying by what it wraps keeps the
historical shortcut for it exactly as before delegation was
considered. The trade-off is the mirror image: a DelegatingDeserializer
subclass that special-cases "" itself, rather than merely forwarding,
is now classified by what it wraps rather than by itself.
@JsonDeserialize(contentConverter = ...) is unaffected: it already
resolves to Jackson's own StdDelegatingDeserializer (Jackson 3:
StdConvertingDeserializer, renamed) for the wrapped value, which was
already classified as standard without any unwrapping. Documented on
both modules' mapBlankStringToNull javadoc and in the README's
blank-strings section, alongside the new innermost-delegate rule.
Tests on both stacks: a BeanDeserializerModifier-installed pass-through
wrapper around Jackson's own BeanDeserializer now yields undefined()/a
present null for "" instead of throwing (confirmed failing on the
pre-change code); the same wrapper around an application-registered
deserializer still lets it decide; and @JsonDeserialize(contentConverter
= ...) keeps skipping the converter for "" and invoking it otherwise.
Reported by @Thorsrud22 in review of #187.
* Classify by nearest concrete or abstract ancestor
isJacksonOwnDeserializer treated any class name outside Jackson's own
namespace as application-owned. That misclassified two shapes it never
considered:
- An application subclass of one of Jackson's own CONCRETE
deserializers (EnumDeserializer, BeanDeserializer,
NumberDeserializers.NumberDeserializer) that changes nothing about
deserialize() - the subclass exists to specialise something else,
e.g. a BeanDeserializerModifier hook. Its own class name is not under
Jackson's namespace, so it was classified as application-owned and
the blank-string shortcut stopped running: undefined() became
InvalidFormatException (enum, bean) or a present null instead of
undefined() (number).
- JsonNullable<JsonNullable<String>>: the outer's content deserializer
is another instance of this library's own class, whose package is
not under Jackson's namespace either, so the outer stopped
shortcutting "" and let it fall through to the inner JsonNullable,
turning undefined() into of(of("")).
Fix: from the innermost deserializer (after the existing getDelegatee()
unwrap), walk up the superclass chain to the nearest ancestor whose
class is either this library's own JsonNullableJackson{2,3}Deserializer
(matched by identity, not by package prefix - this project's own test
fixtures live in the library's package for package-private access, and
a prefix match misclassified all of them as "the library's own" too,
breaking every existing custom-deserializer test; measured directly
before settling on identity) or is under Jackson's own namespace. This
library's own class is always standard. A Jackson ancestor that is
abstract (JsonDeserializer, StdDeserializer, StdScalarDeserializer, ...)
means the application implemented deserialization itself (the #46
authoring style) and stays application-owned. A Jackson ancestor that
is concrete (EnumDeserializer, BeanDeserializer,
NumberDeserializers.NumberDeserializer, ...) means the subclass adds
nothing to Jackson's own blank-string handling and is standard.
Documented, not fixed: a plain forwarding wrapper - a JsonDeserializer/
ValueDeserializer subclass that holds the delegate in a field without
extending DelegatingDeserializer or overriding getDelegatee() - around
Jackson's own Bean/Enum/Collection/Map deserializer cannot be told
apart from an application deserializer built from scratch; it is
classified by its own class. README and both modules'
mapBlankStringToNull javadoc now say a wrapper should extend
DelegatingDeserializer to be seen through.
Also worth stating for the release notes: a subclass of the abstract
FromStringDeserializer is application-owned under this rule and
receives the token; FromStringDeserializer's own template maps a blank
string to null before the subclass's _deserialize hook runs, so such a
property yields of(null) where earlier versions returned undefined().
Measured on both stacks.
Tests on both stacks: an application subclass of EnumDeserializer
registered via addDeserializer and of BeanDeserializer installed by a
BeanDeserializerModifier/ValueDeserializerModifier (undefined() and, with
the option on, of(null)); a subclass of NumberDeserializers.NumberDeserializer
for a JsonNullable<Number> (undefined()); and JsonNullable<JsonNullable<String>>
(undefined() for "", of(of("x")) for "x", proving the nesting itself still
works). All four failed on the pre-fix code before this commit: the enum
and bean cases threw (InvalidFormatException, plus a Jackson 3
NullPointerException from EnumDeserializer's own toString-lookup path
once the token reached it), the number case returned a present null
instead of undefined(), and the nested case returned of(of("")) instead
of undefined().
Found by an independent case sweep after @Thorsrud22's review of #187.1 parent 649a6c1 commit c7bdbe1
9 files changed
Lines changed: 1280 additions & 12 deletions
File tree
- src
- main/java/org/openapitools/jackson/nullable
- test/java/org/openapitools/jackson/nullable
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
95 | 98 | | |
96 | 99 | | |
97 | 100 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
113 | 113 | | |
114 | 114 | | |
115 | 115 | | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
116 | 125 | | |
117 | 126 | | |
118 | 127 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
21 | | - | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
22 | 66 | | |
23 | 67 | | |
24 | 68 | | |
| |||
37 | 81 | | |
38 | 82 | | |
39 | 83 | | |
40 | | - | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
41 | 183 | | |
| 184 | + | |
42 | 185 | | |
43 | 186 | | |
44 | 187 | | |
| |||
50 | 193 | | |
51 | 194 | | |
52 | 195 | | |
53 | | - | |
| 196 | + | |
54 | 197 | | |
55 | 198 | | |
56 | 199 | | |
| |||
0 commit comments