Skip to content

Commit 1ec45c6

Browse files
James Bodkinbclozel
James Bodkin
authored andcommitted
Add Jackson module to serialize/deserialize FieldValue
This commit adds a new `GraphQlModule` Jackson module that supports: * serialization/deserialization of `FieldValue<T>` * the upgrade of `FieldValue<T>` as reference types This enables `FieldValue<T>` to be read/written from/to JSON with Jackson on the client side. Note, on the server side this module is not involved as the payload is deserialized as a `Map<String,Object>` and values are bound as `FieldValue<T>` types directly. Closes gh-1174 Signed-off-by: James Bodkin <[email protected]> [[email protected]: apply code conventions, use FieldValue type] Signed-off-by: Brian Clozel <[email protected]>
1 parent 9b9761f commit 1ec45c6

File tree

9 files changed

+532
-0
lines changed

9 files changed

+532
-0
lines changed

spring-graphql-docs/modules/ROOT/pages/client.adoc

+24
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,30 @@ include-code::UseInterceptor[tag=register,indent=0]
393393

394394

395395

396+
[[client.argument-value]
397+
== Argument Value
398+
399+
If you want to use `ArgumentValue` from a client or test, you can register the
400+
`GraphQLModule` in Jackson which can serialize/deserialize the value depending on
401+
the state.
402+
403+
For example:
404+
405+
[source,java,indent=0,subs="verbatim,quotes"]
406+
----
407+
@Configuration
408+
public class MyConfiguration {
409+
410+
@Bean
411+
public GraphQLModule graphQLModule() {
412+
return new GraphQLModule();
413+
}
414+
415+
}
416+
----
417+
418+
419+
396420
[[client.dgsgraphqlclient]]
397421
== DGS Codegen
398422

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.client.json;
18+
19+
import java.io.Serial;
20+
import java.io.Serializable;
21+
22+
import com.fasterxml.jackson.databind.DeserializationContext;
23+
import com.fasterxml.jackson.databind.JavaType;
24+
import com.fasterxml.jackson.databind.JsonDeserializer;
25+
import com.fasterxml.jackson.databind.JsonMappingException;
26+
import com.fasterxml.jackson.databind.deser.ValueInstantiator;
27+
import com.fasterxml.jackson.databind.deser.std.ReferenceTypeDeserializer;
28+
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
29+
import com.fasterxml.jackson.databind.ser.std.ReferenceTypeSerializer;
30+
31+
import org.springframework.graphql.FieldValue;
32+
import org.springframework.lang.Nullable;
33+
34+
/**
35+
* {@link ReferenceTypeSerializer} that deserializes JSON values as {@link FieldValue}:
36+
* <ul>
37+
* <li>a {@link FieldValue#isEmpty() non empty FieldValue} when the JSON key is present and its value is not {@literal null}.
38+
* <li>an {@link FieldValue#isEmpty() empty FieldValue} when the JSON key is present and its value is {@literal null}.
39+
* <li>an {@link FieldValue#isOmitted() ommitted FieldValue} when the JSON key is not present.
40+
* </ul>
41+
* @author James Bodkin
42+
*/
43+
class FieldValueDeserializer extends ReferenceTypeDeserializer<FieldValue<?>> {
44+
45+
@Serial
46+
private static final long serialVersionUID = 1L;
47+
48+
FieldValueDeserializer(final JavaType fullType, @Nullable final ValueInstantiator vi,
49+
final TypeDeserializer typeDeser, final JsonDeserializer<?> deser) {
50+
51+
super(fullType, vi, typeDeser, deser);
52+
}
53+
54+
@Override
55+
protected ReferenceTypeDeserializer<FieldValue<?>> withResolved(final TypeDeserializer typeDeser,
56+
final JsonDeserializer<?> valueDeser) {
57+
58+
return new FieldValueDeserializer(_fullType, _valueInstantiator, typeDeser, valueDeser);
59+
}
60+
61+
@Override
62+
public FieldValue<? extends Serializable> getNullValue(final DeserializationContext ctxt) throws JsonMappingException {
63+
return FieldValue.ofNullable((Serializable) _valueDeserializer.getNullValue(ctxt));
64+
}
65+
66+
@Override
67+
public Object getEmptyValue(final DeserializationContext ctxt) throws JsonMappingException {
68+
return getNullValue(ctxt);
69+
}
70+
71+
@Override
72+
public Object getAbsentValue(final DeserializationContext ctxt) {
73+
return FieldValue.omitted();
74+
}
75+
76+
@Override
77+
public FieldValue<?> referenceValue(final Object contents) {
78+
return FieldValue.ofNullable((Serializable) contents);
79+
}
80+
81+
@Nullable
82+
@Override
83+
public Object getReferenced(final FieldValue<?> value) {
84+
return value.value();
85+
}
86+
87+
@Override
88+
public FieldValue<?> updateReference(final FieldValue<?> value, final Object contents) {
89+
return FieldValue.ofNullable((Serializable) contents);
90+
}
91+
92+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.client.json;
18+
19+
import java.io.Serial;
20+
21+
import com.fasterxml.jackson.databind.BeanProperty;
22+
import com.fasterxml.jackson.databind.JsonSerializer;
23+
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
24+
import com.fasterxml.jackson.databind.ser.std.ReferenceTypeSerializer;
25+
import com.fasterxml.jackson.databind.type.ReferenceType;
26+
import com.fasterxml.jackson.databind.util.NameTransformer;
27+
28+
import org.springframework.graphql.FieldValue;
29+
import org.springframework.lang.Nullable;
30+
31+
/**
32+
* {@link ReferenceTypeSerializer} that serializes {@link FieldValue} values as:
33+
* <ul>
34+
* <li>the embedded value if it is present and not {@literal null}.
35+
* <li>{@literal null} if the embedded value is present and {@literal null}.
36+
* <li>an empty value if the embedded value is not present.
37+
* </ul>
38+
* @author James Bodkin
39+
*/
40+
class FieldValueSerializer extends ReferenceTypeSerializer<FieldValue<?>> {
41+
42+
@Serial
43+
private static final long serialVersionUID = 1L;
44+
45+
FieldValueSerializer(final ReferenceType fullType, final boolean staticTyping,
46+
@Nullable final TypeSerializer vts, final JsonSerializer<Object> ser) {
47+
48+
super(fullType, staticTyping, vts, ser);
49+
}
50+
51+
protected FieldValueSerializer(final FieldValueSerializer base, final BeanProperty property,
52+
final TypeSerializer vts, final JsonSerializer<?> valueSer,
53+
final NameTransformer unwrapper, final Object suppressableValue,
54+
final boolean suppressNulls) {
55+
56+
super(base, property, vts, valueSer, unwrapper, suppressableValue, suppressNulls);
57+
}
58+
59+
@Override
60+
protected ReferenceTypeSerializer<FieldValue<?>> withResolved(final BeanProperty prop, final TypeSerializer vts,
61+
final JsonSerializer<?> valueSer, final NameTransformer unwrapper) {
62+
63+
return new FieldValueSerializer(this, prop, vts, valueSer, unwrapper, _suppressableValue, _suppressNulls);
64+
}
65+
66+
@Override
67+
public ReferenceTypeSerializer<FieldValue<?>> withContentInclusion(final Object suppressableValue, final boolean suppressNulls) {
68+
return new FieldValueSerializer(this, _property, _valueTypeSerializer,
69+
_valueSerializer, _unwrapper, suppressableValue, suppressNulls);
70+
}
71+
72+
@Override
73+
protected boolean _isValuePresent(final FieldValue<?> value) {
74+
return !value.isOmitted();
75+
}
76+
77+
@Nullable
78+
@Override
79+
protected Object _getReferenced(final FieldValue<?> value) {
80+
return value.value();
81+
}
82+
83+
@Nullable
84+
@Override
85+
protected Object _getReferencedIfPresent(final FieldValue<?> value) {
86+
return value.value();
87+
}
88+
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.client.json;
18+
19+
import java.lang.reflect.Type;
20+
21+
import com.fasterxml.jackson.databind.JavaType;
22+
import com.fasterxml.jackson.databind.type.ReferenceType;
23+
import com.fasterxml.jackson.databind.type.TypeBindings;
24+
import com.fasterxml.jackson.databind.type.TypeFactory;
25+
import com.fasterxml.jackson.databind.type.TypeModifier;
26+
27+
import org.springframework.graphql.FieldValue;
28+
29+
/**
30+
* {@link TypeModifier} that upgrades {@link FieldValue} types to {@link ReferenceType}.
31+
*
32+
* @author James Bodkin
33+
*/
34+
class FieldValueTypeModifier extends TypeModifier {
35+
36+
@Override
37+
public JavaType modifyType(final JavaType type, final Type jdkType, final TypeBindings context, final TypeFactory typeFactory) {
38+
Class<?> raw = type.getRawClass();
39+
if (!type.isReferenceType() && !type.isContainerType() && raw == FieldValue.class) {
40+
JavaType refType = type.containedTypeOrUnknown(0);
41+
return ReferenceType.upgradeFrom(type, refType);
42+
}
43+
else {
44+
return type;
45+
}
46+
}
47+
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.client.json;
18+
19+
import com.fasterxml.jackson.databind.BeanDescription;
20+
import com.fasterxml.jackson.databind.DeserializationConfig;
21+
import com.fasterxml.jackson.databind.JsonDeserializer;
22+
import com.fasterxml.jackson.databind.deser.Deserializers;
23+
import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
24+
import com.fasterxml.jackson.databind.type.ReferenceType;
25+
26+
import org.springframework.graphql.FieldValue;
27+
import org.springframework.lang.Nullable;
28+
29+
class GraphQlDeserializers extends Deserializers.Base {
30+
31+
@Nullable
32+
@Override
33+
public JsonDeserializer<?> findReferenceDeserializer(final ReferenceType refType, final DeserializationConfig config,
34+
final BeanDescription beanDesc, final TypeDeserializer contentTypeDeserializer,
35+
final JsonDeserializer<?> contentDeserializer) {
36+
37+
if (refType.hasRawClass(FieldValue.class)) {
38+
return new FieldValueDeserializer(refType, null, contentTypeDeserializer, contentDeserializer);
39+
}
40+
else {
41+
return null;
42+
}
43+
}
44+
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.client.json;
18+
19+
import com.fasterxml.jackson.annotation.JsonInclude;
20+
import com.fasterxml.jackson.core.Version;
21+
import com.fasterxml.jackson.databind.Module;
22+
import com.fasterxml.jackson.databind.type.ReferenceType;
23+
24+
import org.springframework.graphql.FieldValue;
25+
26+
/**
27+
* {@link Module Jackson module} for JSON support in GraphQL clients.
28+
* <p>This module ships with the following features:
29+
* <ul>
30+
* <li>Manage {@link FieldValue} types as {@link ReferenceType}, similar to {@link java.util.Optional}
31+
* <li>Serializing and Deserializing values contained in {@link FieldValue} reference types
32+
* </ul>
33+
* @author James Bodkin
34+
* @since 1.4.0
35+
*/
36+
public class GraphQlModule extends Module {
37+
38+
@Override
39+
public String getModuleName() {
40+
return GraphQlModule.class.getName();
41+
}
42+
43+
@Override
44+
public Version version() {
45+
return Version.unknownVersion();
46+
}
47+
48+
@Override
49+
public void setupModule(final SetupContext context) {
50+
context.addSerializers(new GraphQlSerializers());
51+
context.addDeserializers(new GraphQlDeserializers());
52+
context.addTypeModifier(new FieldValueTypeModifier());
53+
54+
context.configOverride(FieldValue.class)
55+
.setInclude(JsonInclude.Value.empty().withValueInclusion(JsonInclude.Include.NON_ABSENT));
56+
}
57+
58+
}

0 commit comments

Comments
 (0)