Skip to content

Commit 06e8208

Browse files
authored
Merge pull request #15212 from codeconsole/7.0.x-simple-enum-json-support
7.0.x - Fix Enum JSON/XML Serialization for Round-Trip Compatibility
2 parents 5d78f8d + dbe8c7b commit 06e8208

File tree

7 files changed

+240
-5
lines changed

7 files changed

+240
-5
lines changed

grails-converters/src/main/groovy/org/grails/web/converters/configuration/ConvertersConfigurationInitializer.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,19 @@ private void initJSONConfiguration() {
9595
marshallers.add(new org.grails.web.converters.marshaller.json.ByteArrayMarshaller());
9696
marshallers.add(new org.grails.web.converters.marshaller.json.CollectionMarshaller());
9797
marshallers.add(new org.grails.web.converters.marshaller.json.MapMarshaller());
98-
marshallers.add(new org.grails.web.converters.marshaller.json.EnumMarshaller());
99-
marshallers.add(new org.grails.web.converters.marshaller.ProxyUnwrappingMarshaller<>());
10098

10199
Config grailsConfig = getGrailsConfig();
102100

101+
// Register enum marshaller - defaults to legacy for backward compatibility (will change in Grails 8.0)
102+
String jsonEnumFormat = grailsConfig.getProperty("grails.converters.json.enum.format", String.class, "default");
103+
if ("simple".equals(jsonEnumFormat)) {
104+
marshallers.add(new org.grails.web.converters.marshaller.json.SimpleEnumMarshaller());
105+
} else {
106+
marshallers.add(new org.grails.web.converters.marshaller.json.EnumMarshaller());
107+
}
108+
109+
marshallers.add(new org.grails.web.converters.marshaller.ProxyUnwrappingMarshaller<>());
110+
103111
if ("javascript".equals(grailsConfig.getProperty(SETTING_CONVERTERS_JSON_DATE, String.class, "default", Arrays.asList("javascript", "default")))) {
104112
if (LOG.isDebugEnabled()) {
105113
LOG.debug("Using Javascript JSON Date Marshaller.");
@@ -177,14 +185,22 @@ private void initXMLConfiguration() {
177185
marshallers.add(new org.grails.web.converters.marshaller.xml.ArrayMarshaller());
178186
marshallers.add(new org.grails.web.converters.marshaller.xml.CollectionMarshaller());
179187
marshallers.add(new org.grails.web.converters.marshaller.xml.MapMarshaller());
180-
marshallers.add(new org.grails.web.converters.marshaller.xml.EnumMarshaller());
188+
189+
Config grailsConfig = getGrailsConfig();
190+
191+
// Register enum marshaller - defaults to legacy for backward compatibility (will change in Grails 8.0)
192+
String xmlEnumFormat = grailsConfig.getProperty("grails.converters.xml.enum.format", String.class, "default");
193+
if ("simple".equals(xmlEnumFormat)) {
194+
marshallers.add(new org.grails.web.converters.marshaller.xml.SimpleEnumMarshaller());
195+
} else {
196+
marshallers.add(new org.grails.web.converters.marshaller.xml.EnumMarshaller());
197+
}
198+
181199
marshallers.add(new org.grails.web.converters.marshaller.xml.DateMarshaller());
182200
marshallers.add(new ProxyUnwrappingMarshaller<>());
183201
marshallers.add(new org.grails.web.converters.marshaller.xml.ToStringBeanMarshaller());
184202
ProxyHandler proxyHandler = getProxyHandler();
185203

186-
Config grailsConfig = getGrailsConfig();
187-
188204
boolean includeDomainVersion = includeDomainVersionProperty(grailsConfig, "xml");
189205
if (grailsConfig.getProperty(SETTING_CONVERTERS_XML_DEEP, Boolean.class, false)) {
190206
marshallers.add(new org.grails.web.converters.marshaller.xml.DeepDomainClassMarshaller(includeDomainVersion, proxyHandler, grailsApplication));

grails-converters/src/main/groovy/org/grails/web/converters/marshaller/json/EnumMarshaller.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@
3030
/**
3131
* @author Siegfried Puchbauer
3232
* @since 1.1
33+
* @deprecated As of 7.0.2, replaced by {@link SimpleEnumMarshaller} for round-trip compatibility.
34+
* This marshaller will no longer be registered by default in Grails 8.0.
35+
* To opt-in to the new behavior now, set {@code grails.converters.json.enum.format=simple} in application.yml.
3336
*/
37+
@Deprecated(forRemoval = true, since = "7.0.2")
3438
public class EnumMarshaller implements ObjectMarshaller<JSON> {
3539

3640
public boolean supports(Object object) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.grails.web.converters.marshaller.json;
20+
21+
import java.lang.reflect.Method;
22+
23+
import org.springframework.beans.BeanUtils;
24+
25+
import grails.converters.JSON;
26+
import org.grails.web.converters.exceptions.ConverterException;
27+
import org.grails.web.converters.marshaller.ObjectMarshaller;
28+
29+
/**
30+
* Marshals enums as simple string values (just the enum name) for symmetric serialization/deserialization.
31+
* This provides round-trip compatibility where POSTing JSON returns the same format when GETting.
32+
*
33+
* @since 7.0.2
34+
*/
35+
public class SimpleEnumMarshaller implements ObjectMarshaller<JSON> {
36+
37+
public boolean supports(Object object) {
38+
return object.getClass().isEnum();
39+
}
40+
41+
public void marshalObject(Object en, JSON json) throws ConverterException {
42+
try {
43+
Method nameMethod = BeanUtils.findDeclaredMethod(en.getClass(), "name");
44+
try {
45+
json.convertAnother(nameMethod.invoke(en));
46+
}
47+
catch (Exception e) {
48+
json.convertAnother("");
49+
}
50+
}
51+
catch (ConverterException ce) {
52+
throw ce;
53+
}
54+
catch (Exception e) {
55+
throw new ConverterException("Error converting Enum with class " + en.getClass().getName(), e);
56+
}
57+
}
58+
}

grails-converters/src/main/groovy/org/grails/web/converters/marshaller/xml/EnumMarshaller.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@
2929
/**
3030
* @author Siegfried Puchbauer
3131
* @since 1.1
32+
* @deprecated As of 7.0.2, replaced by {@link SimpleEnumMarshaller} for round-trip compatibility.
33+
* This marshaller will no longer be registered by default in Grails 8.0.
34+
* To opt-in to the new behavior now, set {@code grails.converters.xml.enum.format=simple} in application.yml.
3235
*/
36+
@Deprecated(forRemoval = true, since = "7.0.2")
3337
public class EnumMarshaller implements ObjectMarshaller<XML> {
3438

3539
public boolean supports(Object object) {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.grails.web.converters.marshaller.xml;
20+
21+
import java.lang.reflect.Method;
22+
23+
import org.springframework.beans.BeanUtils;
24+
25+
import grails.converters.XML;
26+
import org.grails.web.converters.exceptions.ConverterException;
27+
import org.grails.web.converters.marshaller.ObjectMarshaller;
28+
29+
/**
30+
* Marshals enums as simple string values (just the enum name) for symmetric serialization/deserialization.
31+
* This provides round-trip compatibility where POSTing XML returns the same format when GETting.
32+
*
33+
* @since 7.0.2
34+
*/
35+
public class SimpleEnumMarshaller implements ObjectMarshaller<XML> {
36+
37+
public boolean supports(Object object) {
38+
return object.getClass().isEnum();
39+
}
40+
41+
public void marshalObject(Object en, XML xml) throws ConverterException {
42+
try {
43+
Method nameMethod = BeanUtils.findDeclaredMethod(en.getClass(), "name");
44+
try {
45+
xml.chars(nameMethod.invoke(en).toString());
46+
}
47+
catch (Exception e) {
48+
// ignored
49+
}
50+
}
51+
catch (ConverterException ce) {
52+
throw ce;
53+
}
54+
catch (Exception e) {
55+
throw new ConverterException("Error converting Enum with class " + en.getClass().getName(), e);
56+
}
57+
}
58+
}

grails-doc/src/en/guide/upgrading/upgrading60x.adoc

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,3 +683,74 @@ If your application or API consumers depend on the previous JSON format for `Cal
683683
4. **ZonedDateTime**: Note that the zone ID (e.g., `[America/Los_Angeles]`) is no longer included in the output, matching Spring Boot's behavior.
684684

685685
This change applies to both the `grails-converters` module (standard JSON rendering) and the `grails-views-gson` module (JSON views).
686+
687+
===== 12.26 Enum JSON/XML Serialization
688+
689+
As of Grails 7.0.2, enum serialization has been enhanced to support round-trip compatibility between JSON/XML serialization and deserialization. The legacy enum marshaller, which produced verbose output with type metadata, has been deprecated in favor of a simpler format that matches how enums are expected on input.
690+
691+
====== Legacy Behavior (Deprecated)
692+
693+
Previously, enums were serialized with metadata:
694+
695+
*JSON:*
696+
[source,json]
697+
----
698+
{
699+
"stage": {
700+
"enumType": "com.example.ChallengeStage",
701+
"name": "SUBMIT"
702+
}
703+
}
704+
----
705+
706+
*XML:*
707+
[source,xml]
708+
----
709+
<stage enumType="com.example.ChallengeStage">SUBMIT</stage>
710+
----
711+
712+
This format is **asymmetric** - when POSTing data, you send `"stage":"SUBMIT"`, but when GETting data, you receive the verbose object structure.
713+
714+
====== New Behavior (Recommended)
715+
716+
The new `SimpleEnumMarshaller` serializes enums as simple string values, providing **round-trip compatibility**:
717+
718+
*JSON:*
719+
[source,json]
720+
----
721+
{
722+
"stage": "SUBMIT"
723+
}
724+
----
725+
726+
*XML:*
727+
[source,xml]
728+
----
729+
<stage>SUBMIT</stage>
730+
----
731+
732+
Now the format you POST is the same format you GET back.
733+
734+
====== Migration
735+
736+
To opt-in to the new behavior, add the following to your `application.yml`:
737+
738+
[source,yml]
739+
.application.yml
740+
----
741+
grails:
742+
converters:
743+
json:
744+
enum:
745+
format: simple
746+
xml:
747+
enum:
748+
format: simple
749+
----
750+
751+
====== Deprecation Timeline
752+
753+
* **7.0.2**: Legacy `EnumMarshaller` deprecated (default), `SimpleEnumMarshaller` available via config
754+
* **8.0**: `SimpleEnumMarshaller` will become the default
755+
756+
The legacy `org.grails.web.converters.marshaller.json.EnumMarshaller` and `org.grails.web.converters.marshaller.xml.EnumMarshaller` classes are marked as `@Deprecated(forRemoval = true, since = "7.0.2")` and will be removed in Grails 8.0.

grails-test-suite-web/src/test/groovy/org/grails/web/converters/JSONConverterTests.groovy

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,26 @@ class JSONConverterTests extends Specification implements ControllerUnitTest<JSO
103103
json.size() == 2
104104
}
105105

106+
void testJSONEnumConvertingWithSimpleMarshaller() {
107+
given:
108+
JSON.createNamedConfig('simple') {
109+
it.registerObjectMarshaller(new org.grails.web.converters.marshaller.json.SimpleEnumMarshaller())
110+
}
111+
JSON.use('simple')
112+
113+
when:
114+
def enumInstance = Role.HEAD
115+
params.e = enumInstance
116+
controller.testEnumInMap()
117+
def jsonString = response.contentAsString
118+
def json = response.json
119+
120+
then:
121+
json.size() == 1
122+
jsonString == '{"value":"HEAD"}'
123+
json.value == "HEAD"
124+
}
125+
106126
// GRAILS-11513
107127
void testStringsWithQuotes() {
108128
when:
@@ -196,6 +216,10 @@ class JSONConverterController {
196216
render params.e as JSON
197217
}
198218

219+
def testEnumInMap = {
220+
render([value: params.e] as JSON)
221+
}
222+
199223
def testNullValues = {
200224
def descriptors = [:]
201225
descriptors.put(null,null)

0 commit comments

Comments
 (0)