codeconsole opened a new pull request, #16209:
URL: https://github.com/apache/grails-core/pull/16209
### Problem
A domain property declared as a **raw** collection loses its data during
binding on 8.0.0-M6. Given:
```groovy
class TopicType {
List statusFilters = [] // no type argument
}
new TopicType(statusFilters: [[label: 'Answered', param: 'status=resolved']])
```
every map element is replaced by an empty `new java.lang.Object()`. With
GORM for MongoDB the write then fails:
```
org.bson.codecs.configuration.CodecConfigurationException:
Can't find a codec for CodecCacheKey{clazz=class java.lang.Object,
types=null}
at CodecExtensions$ListCodec.encode(CodecExtensions.groovy:362)
at BasicCollectionTypeEncoder.encode(BasicCollectionTypeEncoder.groovy:64)
at BsonPersistentEntityCodec.encode(BsonPersistentEntityCodec.groovy:229)
```
The exception is incidental — it is the first thing to notice that the
elements are no longer maps. Without a codec in the path the empty objects
would simply be persisted, so the data is lost either way. This worked on
8.0.0-M5.
### Cause
#15947 changed collection binding so that a `Map` element is instantiated as
the component type and bound through the allowlist, rather than passed to a map
constructor:
```diff
- itemsWhichNeedBinding << item
+ def instance = instantiateAndBindNestedOrUseMapConstructor(referencedType,
item, itemBindingSource, ...)
+ if (instance != null) { itemsWhichNeedBinding << instance }
```
That is correct for a real nested type, but the component type is not always
one. `Basic#componentType` falls back to `Object.class` when a property carries
no generic signature, so a raw collection arrives with `referencedType ==
Object`. `Object` has a public no-arg constructor, so
`getDeclaredConstructor().newInstance()` succeeds, and `Object` declares no
properties, so `bindNested` has nowhere to put the map's contents.
This also escapes the opt-in. The instantiation is in the `try`, while
`isDenyByDefaultEnabled()` guards only the map-constructor fallback in the
`catch` — so an application that never set
`grails.databinding.legacyBindableDefault=false` still loses the data, contrary
to #15947's stated intent that unconfigured applications keep binding
permissively.
### Fix
Treat `Object` (and `null`) as "not a nested type" and keep the element as
it stands:
```groovy
if (referencedType == null || referencedType == Object) {
return value
}
```
Nothing can be mass-assigned through a value that is never used as a
property source, so the hardening loses nothing: every other component type
still instantiates and binds through the allowlist, and `bindable: false` is
unaffected. Applied to `GrailsWebDataBinder` and to `SimpleDataBinder`, which
is exposed to the same value because `getReferencedTypeForCollection` is
overridden by the GORM-aware subclass and dispatches virtually from
`SimpleDataBinder`'s own collection paths.
### Tests
Adds a regression test to `GrailsWebDataBinderSpec` (with a
`RawCollectionContainer` domain). It fails on unmodified `8.0.x` with exactly
the production symptom:
```
[java.lang.Object@7c79f2cf, java.lang.Object@63551c66]
```
and passes with the fix. `:grails-databinding-core:test`,
`:grails-web-databinding:test` (including `DenyByDefaultConfigSpec`) and the
`grails.web.databinding.*` suite in `grails-test-suite-persistence` are all
green.
Also verified end to end against the real application that hit this: with
these two jars built at `8.0.0-M6` and substituted into an otherwise stock M6
app, the raw-collection domain that previously failed now boots and persists
its maps intact.
--
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]