Skip to content

Commit c0abfa8

Browse files
committed
Fix JSON decoding to parameterized types in HttpSyncGraphQlClient
This commit ensures that the most complete type information is provided to the codec when deserializing parameterized types using `HttpMessageConverters` with the sync client. Fixes gh-1383
1 parent fef05db commit c0abfa8

File tree

4 files changed

+123
-5
lines changed

4 files changed

+123
-5
lines changed

spring-graphql/src/main/java/org/springframework/graphql/client/HttpMessageConverterDelegate.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.springframework.http.MediaType;
4545
import org.springframework.http.converter.GenericHttpMessageConverter;
4646
import org.springframework.http.converter.HttpMessageConverter;
47+
import org.springframework.http.converter.SmartHttpMessageConverter;
4748
import org.springframework.util.MimeType;
4849

4950

@@ -112,7 +113,10 @@ public DataBuffer encodeValue(
112113

113114
HttpOutputMessageAdapter messageAdapter = new HttpOutputMessageAdapter();
114115
try {
115-
if (this.converter instanceof GenericHttpMessageConverter<Object> genericConverter) {
116+
if (this.converter instanceof SmartHttpMessageConverter<Object> smartConverter) {
117+
smartConverter.write(value, valueType, toMediaType(mimeType), messageAdapter, hints);
118+
}
119+
else if (this.converter instanceof GenericHttpMessageConverter<Object> genericConverter) {
116120
genericConverter.write(value, valueType.getType(), toMediaType(mimeType), messageAdapter);
117121
}
118122
else {
@@ -166,7 +170,10 @@ public Object decode(DataBuffer buffer, ResolvableType targetType,
166170

167171
try {
168172
HttpInputMessageAdapter messageAdapter = new HttpInputMessageAdapter(buffer);
169-
if (this.converter instanceof GenericHttpMessageConverter<Object> genericConverter) {
173+
if (this.converter instanceof SmartHttpMessageConverter<Object> smartConverter) {
174+
return smartConverter.read(targetType, messageAdapter, hints);
175+
}
176+
else if (this.converter instanceof GenericHttpMessageConverter<Object> genericConverter) {
170177
return genericConverter.read(targetType.getType(), null, messageAdapter);
171178
}
172179
else {

spring-graphql/src/test/java/org/springframework/graphql/client/HttpGraphQlClientProtocolTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
import static org.junit.jupiter.params.provider.Arguments.arguments;
4444

4545
/**
46-
* Tests for {@link GraphQlClient} that check whether it supports
46+
* Tests for {@link HttpGraphQlClient} that check whether it supports
4747
* the GraphQL over HTTP specification.
4848
*
4949
* @see <a href="https://graphql.github.io/graphql-over-http/draft/#sec-application-graphql-response-json">GraphQL over HTTP specification</a>
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2020-present 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;
18+
19+
import java.io.IOException;
20+
import java.util.List;
21+
import java.util.stream.Stream;
22+
23+
import mockwebserver3.MockResponse;
24+
import mockwebserver3.MockWebServer;
25+
import org.junit.jupiter.api.AfterEach;
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.params.ParameterizedTest;
28+
import org.junit.jupiter.params.provider.Arguments;
29+
import org.junit.jupiter.params.provider.MethodSource;
30+
31+
import org.springframework.graphql.Book;
32+
import org.springframework.graphql.MediaTypes;
33+
import org.springframework.http.MediaType;
34+
35+
import static org.assertj.core.api.Assertions.assertThat;
36+
import static org.junit.jupiter.api.Named.named;
37+
import static org.junit.jupiter.params.provider.Arguments.arguments;
38+
39+
/**
40+
* Integration tests for {@link HttpGraphQlClient}.
41+
*/
42+
class HttpGraphQlClientTests {
43+
44+
private MockWebServer server;
45+
46+
@BeforeEach
47+
void setup() throws IOException {
48+
this.server = new MockWebServer();
49+
this.server.start();
50+
}
51+
52+
@AfterEach
53+
void tearDown() throws IOException {
54+
this.server.close();
55+
}
56+
57+
static Stream<Arguments> graphQlClientTypes() {
58+
return Stream.of(
59+
arguments(named("HttpGraphQlClient", ClientType.ASYNC)),
60+
arguments(named("HttpSyncGraphQlClient", ClientType.SYNC))
61+
);
62+
}
63+
64+
@ParameterizedTest
65+
@MethodSource("graphQlClientTypes")
66+
void toEntityListSync(ClientType clientType) {
67+
prepareOkResponse("""
68+
{
69+
"data": {
70+
"favoriteBooks": [
71+
{"id":"42","name":"Hitchhiker's Guide to the Galaxy"},
72+
{"id":"53","name":"Breaking Bad"}
73+
]
74+
}
75+
}
76+
""");
77+
List<Book> favoriteBooks = createClient(clientType).document("{ favoriteBooks() { id name } }")
78+
.retrieveSync("favoriteBooks")
79+
.toEntityList(Book.class);
80+
81+
assertThat(favoriteBooks).hasSize(2);
82+
assertThat(favoriteBooks.get(0)).isInstanceOf(Book.class);
83+
assertThat(favoriteBooks).extracting("id").contains(42L, 53L);
84+
}
85+
86+
private GraphQlClient createClient(ClientType clientType) {
87+
return switch (clientType) {
88+
case ASYNC -> HttpGraphQlClient.builder().url(server.url("/").toString()).build();
89+
case SYNC -> HttpSyncGraphQlClient.builder().url(server.url("/").toString()).build();
90+
};
91+
}
92+
93+
void prepareOkResponse(String body) {
94+
prepareResponse(200, MediaTypes.APPLICATION_GRAPHQL_RESPONSE, body);
95+
}
96+
97+
private void prepareResponse(int status, MediaType contentType, String body) {
98+
MockResponse mockResponse = new MockResponse.Builder().code(status)
99+
.setHeader("Content-Type", contentType.toString())
100+
.body(body)
101+
.build();
102+
this.server.enqueue(mockResponse);
103+
}
104+
105+
enum ClientType {
106+
ASYNC, SYNC
107+
}
108+
109+
}

spring-graphql/src/test/java/org/springframework/graphql/client/HttpSyncGraphQlClientBuilderTests.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
import java.io.IOException;
2020
import java.net.URI;
2121
import java.util.Collections;
22+
import java.util.Map;
2223

2324
import graphql.ExecutionResultImpl;
2425
import org.jspecify.annotations.Nullable;
2526
import org.junit.jupiter.api.Test;
2627
import reactor.core.publisher.Mono;
2728

29+
import org.springframework.core.ResolvableType;
2830
import org.springframework.graphql.execution.MockExecutionGraphQlService;
2931
import org.springframework.graphql.server.WebGraphQlHandler;
3032
import org.springframework.graphql.server.WebGraphQlInterceptor;
@@ -305,8 +307,8 @@ Object getLastValue() {
305307
}
306308

307309
@Override
308-
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
309-
this.lastValue = super.readInternal(clazz, inputMessage);
310+
public Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints) throws IOException, HttpMessageNotReadableException {
311+
this.lastValue = super.read(type, inputMessage, hints);
310312
return this.lastValue;
311313
}
312314

0 commit comments

Comments
 (0)