codeconsole commented on code in PR #16414:
URL: https://github.com/apache/grails-core/pull/16414#discussion_r4213492172
##########
grails-converters/src/main/groovy/org/grails/web/converters/configuration/ConvertersConfigurationInitializer.java:
##########
@@ -97,6 +116,8 @@ public void initialize() {
if (applicationContext != null) {
ConvertersConfigurationHolder.setObservationRegistry(
applicationContext.getBeanProvider(ObservationRegistry.class).getIfAvailable(()
-> ObservationRegistry.NOOP));
+ // the application's JsonMapper, or a default one when there is
none, or more than one and none is primary
+
ConvertersConfigurationHolder.setJsonMapper(applicationContext.getBeanProvider(JsonMapper.class).getIfUnique());
Review Comment:
Fixed in e41e7e431d. With the switch on, the initializer installs a default
`JsonMapper` rather than the application's. `Grails8JsonRenderingSpec` uses a
mapper with:
- `INDENT_OUTPUT`;
- `WRITE_NUMBERS_AS_STRINGS`;
- `WRITE_BIGDECIMAL_AS_PLAIN`;
- a New York time zone;
- a module serializer.
With that mapper, every value, both pretty-print layouts and all 11 domain
scenarios still match the 8.0.x text. `toString(false)` with `pretty.print:
true` indents again, as 8.0.x ignored the argument.
##########
grails-converters/src/main/groovy/grails/converters/JSON.java:
##########
@@ -220,6 +228,26 @@ else if (o instanceof Number) {
}
}
+ /**
+ * @return the JsonMapper this converter writes with
+ * @since 9.0
+ */
+ public JsonMapperSupport getJsonMapper() {
+ return jsonMapper != null ? jsonMapper :
ConvertersConfigurationHolder.getJsonMapper();
+ }
+
+ /**
+ * The JSON object key for a map key, as the JsonMapper writes it: a
{@code String} key as it is, and a
+ * {@code Date} key, for example, in the mapper's date format.
+ *
+ * @param key a non-null map key
+ * @return the JSON object key
+ * @since 9.0
+ */
+ public String formatKey(Object key) {
+ return getJsonMapper().formatKey(key);
Review Comment:
Fixed in e41e7e431d. In legacy mode `JSON.formatKey` uses
`JsonDateFormat.formatKey`. One of the domain scenarios renders `Class`, `Date`
and `@JsonValue` keys, and its expected text comes from 8.0.x: `"class
java.lang.String"`, the UTC instant, and the key's `toString()`.
The `Class` key change in the new mode is now in the guide's map-key
paragraph (1b644c8973), along with `@JsonValue` keys, and
`ZonedDateTime`/`OffsetDateTime` keys keeping their own offset.
##########
grails-converters/src/main/groovy/org/grails/web/converters/configuration/ConvertersConfigurationInitializer.java:
##########
@@ -111,26 +132,75 @@ private void initJSONConfiguration() {
List<ObjectMarshaller<JSON>> marshallers = new ArrayList<>();
marshallers.addAll(getPreviouslyConfiguredMarshallers(JSON.class));
- marshallers.add(new
org.grails.web.converters.marshaller.json.ArrayMarshaller());
- marshallers.add(new
org.grails.web.converters.marshaller.json.ByteArrayMarshaller());
- marshallers.add(new
org.grails.web.converters.marshaller.json.CollectionMarshaller());
- marshallers.add(new
org.grails.web.converters.marshaller.json.MapMarshaller());
- marshallers.add(new
org.grails.web.converters.marshaller.json.SimpleEnumMarshaller());
Config grailsConfig = getGrailsConfig();
+ ProxyHandler proxyHandler = getProxyHandler();
+ boolean legacy =
grailsConfig.getProperty(SETTING_CONVERTERS_JSON_LEGACY, Boolean.class, false);
+ ConvertersConfigurationHolder.setLegacyJson(legacy);
+ if (legacy) {
+ LOG.warn(SETTING_CONVERTERS_JSON_LEGACY + " renders JSON as Grails
8 did. It is deprecated, and will be removed in Grails 10.");
+ addLegacyJsonMarshallers(marshallers, grailsConfig, proxyHandler);
+ }
+ else {
+ addJsonMarshallers(marshallers, grailsConfig, proxyHandler);
+ }
- marshallers.add(new
org.grails.web.converters.marshaller.ProxyUnwrappingMarshaller<>());
+ DefaultConverterConfiguration<JSON> cfg = new
DefaultConverterConfiguration<>(marshallers, proxyHandler);
+ cfg.setEncoding(grailsConfig.getProperty(SETTING_CONVERTERS_ENCODING,
"UTF-8"));
+ String defaultCirRefBehaviour =
grailsConfig.getProperty(SETTING_CONVERTERS_CIRCULAR_REFERENCE_BEHAVIOUR,
"DEFAULT");
+
cfg.setCircularReferenceBehaviour(Converter.CircularReferenceBehaviour.valueOf(
+
grailsConfig.getProperty(SETTING_CONVERTERS_JSON_CIRCULAR_REFERENCE_BEHAVIOUR,
String.class,
+ defaultCirRefBehaviour,
Converter.CircularReferenceBehaviour.allowedValues())));
+
+ Boolean defaultPrettyPrint =
grailsConfig.getProperty(SETTING_CONVERTERS_PRETTY_PRINT, Boolean.class, false);
+ Boolean prettyPrint =
grailsConfig.getProperty(SETTING_CONVERTERS_JSON_PRETTY_PRINT, Boolean.class,
defaultPrettyPrint);
+ cfg.setPrettyPrint(prettyPrint);
+
cfg.setCacheObjectMarshallerByClass(grailsConfig.getProperty(SETTING_CONVERTERS_JSON_CACHE_OBJECTS,
Boolean.class, true));
+
+ registerObjectMarshallersFromApplicationContext(cfg, JSON.class);
+ ConvertersConfigurationHolder.setDefaultConfiguration(JSON.class, new
ChainedConverterConfiguration<>(cfg, proxyHandler));
+ }
+
+ private void addJsonMarshallers(List<ObjectMarshaller<JSON>> marshallers,
Config grailsConfig, ProxyHandler proxyHandler) {
if
("javascript".equals(grailsConfig.getProperty(SETTING_CONVERTERS_JSON_DATE,
String.class, "default", Arrays.asList("javascript", "default")))) {
if (LOG.isDebugEnabled()) {
LOG.debug("Using Javascript JSON Date Marshaller.");
}
marshallers.add(new
org.grails.web.converters.marshaller.json.JavascriptDateMarshaller());
}
+ // domain classes, and proxies of them, are claimed first: the
application's JsonMapper may have a serializer for
+ // them, and they are rendered with the converter's includes, excludes
and deep setting
+ marshallers.add(new
org.grails.web.converters.marshaller.ProxyUnwrappingMarshaller<>());
+ marshallers.add(domainClassMarshaller(grailsConfig, proxyHandler));
+ // dates and times and the other single values the application's
JsonMapper has a serializer for are written
+ // by the mapper, as Spring Boot writes them
+ marshallers.add(new
org.grails.web.converters.marshaller.json.JsonMapperValueMarshaller());
Review Comment:
Fixed in 512e8d9fb7 by letting `SimpleEnumMarshaller` claim them.
`rendersValue` is false for an enum whose serializer is a
`JsonValueSerializer`, so your `Status.ACTIVE` renders `"ACTIVE"`, as a map
value and as a single value, and binds back.
`Month` still goes to the mapper, since its serializer isn't a
`JsonValueSerializer`. The guide's "Enums render by `name()`" sentence now
includes enums with a `@JsonValue`, and the `@JsonValue` row in the table says
"other than an enum".
##########
grails-converters/src/main/groovy/org/grails/web/converters/jackson/DomainClassJacksonModule.java:
##########
@@ -0,0 +1,102 @@
+/*
+ * 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
+ *
+ * https://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.grails.web.converters.jackson;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import tools.jackson.core.Version;
+import tools.jackson.databind.BeanDescription;
+import tools.jackson.databind.JacksonModule;
+import tools.jackson.databind.JavaType;
+import tools.jackson.databind.SerializationConfig;
+import tools.jackson.databind.ValueSerializer;
+import tools.jackson.databind.ser.Serializers;
+
+/**
+ * A Jackson module that serializes domain class instances, and proxies of
them, with a {@link DomainClassSerializer},
+ * as {@code grails.converters.JSON} renders them. A Grails application
registers it with the application's
+ * {@code JsonMapper}, so that writing a domain class instance with the
mapper, including returning one from a
+ * Spring MVC controller, renders it as {@code render ... as JSON} does.
+ *
+ * <p>A domain class with its own {@code @JsonSerialize} serializer keeps it.
+ *
+ * @since 9.0
+ */
+public class DomainClassJacksonModule extends JacksonModule {
+
+ private final Supplier<DomainClassRendering> rendering;
+
+ /**
+ * @param rendering how to render domain class instances
+ */
+ public DomainClassJacksonModule(DomainClassRendering rendering) {
+ this(() -> rendering);
+ }
+
+ /**
+ * @param rendering supplies how to render domain class instances, when
the first one is serialized
+ */
+ public DomainClassJacksonModule(Supplier<DomainClassRendering> rendering) {
+ this.rendering = Objects.requireNonNull(rendering, "rendering cannot
be null");
+ }
+
+ @Override
+ public String getModuleName() {
+ return "grails-domain-classes";
+ }
+
+ @Override
+ public Version version() {
+ return Version.unknownVersion();
+ }
+
+ @Override
+ public void setupModule(SetupContext context) {
+ context.addSerializers(new DomainClassSerializers(rendering));
+ }
+
+ private static final class DomainClassSerializers implements Serializers {
+
+ private final Supplier<DomainClassRendering> rendering;
+
+ private volatile DomainClassSerializer serializer;
+
+ private DomainClassSerializers(Supplier<DomainClassRendering>
rendering) {
+ this.rendering = rendering;
+ }
+
+ @Override
+ public ValueSerializer<?> findSerializer(SerializationConfig config,
JavaType type,
+ BeanDescription.Supplier beanDescRef, JsonFormat.Value
formatOverrides) {
+ DomainClassSerializer domainClassSerializer = serializer();
+ return
domainClassSerializer.getRendering().isDomainClass(type.getRawClass()) ?
domainClassSerializer : null;
Review Comment:
Fixed in f520e7dcbf. `findSerializer` returns `null` for a type that
implements `JacksonSerializable`, or whose bean description has a `@JsonValue`
accessor. Jackson then applies those as it does for any class. The spec checks
your `Code` case: it renders `"X1"`.
--
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]