Skip to content

Commit 4aeae1e

Browse files
fix: don't drop data when schema expects a list (apollographql#326)
If the schema's output type is a list (`[Type]`) but the response is not a list, this change wraps the response in a list as a convenience to the user. Without this change, the router's response formatting will silently drop the data. This is probably the right design choice because the Selection syntax is "agnostic" to arrays, so there's no way for the user to indicate how to handle this situation, nor is there a way to detect this situation during validation (at least not until we have a JSON spec to also validation against.)
1 parent 1614afe commit 4aeae1e

File tree

2 files changed

+70
-1
lines changed

2 files changed

+70
-1
lines changed

apollo-router/src/plugins/connectors/response_formatting/engine.rs

+44
Original file line numberDiff line numberDiff line change
@@ -278,4 +278,48 @@ mod tests {
278278
.unwrap()
279279
);
280280
}
281+
282+
#[test]
283+
fn test_list_coercion() {
284+
let schema = Schema::parse_and_validate(
285+
"type Query {hello: [Hello]} interface Hello{id:ID} type Foo implements Hello{id:ID a:ID}",
286+
"schema.graphql",
287+
)
288+
.unwrap();
289+
290+
let document = ExecutableDocument::parse_and_validate(
291+
&schema,
292+
"{hello{__typename id ...on Foo {a}}}",
293+
"op.graphql",
294+
)
295+
.unwrap();
296+
297+
let data = serde_json_bytes::json!({
298+
"hello": {
299+
"id": "123",
300+
"a": "a"
301+
}
302+
});
303+
304+
let mut diagnostics = vec![];
305+
let result = super::execute(
306+
&schema,
307+
&document,
308+
diagnostics.as_mut(),
309+
data.as_object().unwrap().clone(),
310+
);
311+
312+
assert_eq!(
313+
result,
314+
*serde_json_bytes::json!({
315+
"hello": [{
316+
"__typename": "Foo",
317+
"id": "123",
318+
"a": "a"
319+
}]
320+
})
321+
.as_object()
322+
.unwrap()
323+
);
324+
}
281325
}

apollo-router/src/plugins/connectors/response_formatting/result_coercion.rs

+26-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,32 @@ pub(super) fn complete_value<'a, 'b>(
4040
let ty_name = match ty {
4141
Type::Named(name) | Type::NonNullNamed(name) => name,
4242
Type::List(_) | Type::NonNullList(_) => {
43-
field_diagnostic!("List type {ty} resolved to an object")
43+
// If the schema expects a list, but the response is an object,
44+
// we'll wrap it in a list to make a valid response. This might
45+
// be unexpected, but it the alternative is doing nothing and
46+
// letting the router's response validation silently drop the data.
47+
// We'll also emit a diagnostic to help users identify this mismatch.
48+
diagnostics.push(FormattingDiagnostic::for_path(
49+
format!("List type {ty} resolved to an object"),
50+
path,
51+
));
52+
53+
let inner_path = LinkedPathElement {
54+
element: ResponseDataPathElement::ListIndex(0),
55+
next: path,
56+
};
57+
58+
let inner_resolved = JsonValue::Array(vec![resolved.clone()]);
59+
60+
return complete_value(
61+
schema,
62+
document,
63+
diagnostics,
64+
Some(&inner_path),
65+
ty,
66+
&inner_resolved,
67+
fields,
68+
);
4469
}
4570
};
4671

0 commit comments

Comments
 (0)