Mapping GraphQL Responses
Mapping HTTP responses to GraphQL fields
In this guide, you'll learn about:
Why connectors require mapping
Rules for mapping HTTP responses
Examples of common mapping types
Mapping overview
Mapping HTTP responses to your GraphQL schema bridges the gap between the structure of the data returned by your REST APIs and the structure of your supergraph. You map an HTTP response to the GraphQL schema using the Apollo Connectors mapping language in the @connect
directive's selection
field.
For that reason, this process is sometimes referred to as selection mapping.
The mapping language you use for selection mapping is the same mapping language you use in URIs, headers, and request bodies when making HTTP requests.
In the context of selection mapping, the mapping language has one unique feature. When used in selection
, it's assumed that all fields come from the HTTP response body unless otherwise specified.
The following example shows how each line in the selection
corresponds to fields in the GraphQL schema below.
type Query {
products: Products
@connect(
http: { GET: "https://api.example.com/products" }
selection: """
id # 1
variation { # 2
size { # 3
width # 4
height # 5
}
color # 6
}
"""
)
}
type Product {
id: ID! # 1
variation: Variation # 2
}
type Variation {
size: Dimension # 3
color: String # 6
}
type Dimension {
width: Int # 4
height: Int # 5
}
Because of selection
's unique assumption, you can just write id
instead of needing to start with a variable like $this
or $args
.
In selection
, you also get access to the $status
variable, which isn't available anywhere else.
selection
strings can be broken up into multiple lines with GraphQL multiline string syntax ("""
):selection: """
names: {
first: first_name
middle: middle_name
last: last_name
}
"""
Selection mapping rules
The selection
field is responsible for more than just mapping response fields to the schema; it powers the core of each connector, so it has some special rules.
Selections can't be empty
The selection
field isn't allowed to be empty.
You must map at least one field in every connector.
If you have an endpoint that doesn't return any response data, you can map a scalar value using a literal value:
success: $(true)
All schema fields must be mapped
The only way to populate a field from a connector is via selection
, so every field defined in the schema must be mapped at least once in a connector. The exception is fields that are resolved from another subgraph, such as those marked @external
.
Leaf nodes must be scalars
Different connectors can resolve different fields of the same object, so you must specify every field that a given connector resolves. That means you can never map an entire object and expect the fields to be implicitly mapped. You must map all fields explicitly.
1type Query {
2 product(id: ID!): Product
3 @connect(
4 source: "v1"
5 http: { GET: "/products/{$args.id}" }
6 selection: "$.result { id name }"
7 )
8}
9
10type Product {
11 id: ID!
12 name: String!
13 description: String @connect(source: "v1", http: {GET: "/products/{$this.id}/description"}, selection: "$")
14}
Even though the result
field contains all the information needed from the first connector, you can't map just $.result
. You must specify each field—id
and name
—individually. Doing so This enables the query planner to know that the description
field must be fetched from elsewhere.
Selection mapping examples
The following are examples for commonly used selection mappings. See the Mapping Language reference for a complete overview of the mapping language's capabilities.
Basic selection mapping
Given the following JSON response:
{
"id": "1",
"username": "alice",
"email": "alice@example.com"
}
You can create a basic, flat GraphQL type with fields that map to REST endpoint fields of the same names:
1type Query {
2 user(id: ID!): User
3 @connect(
4 http: { GET: "https://api.example.com/users/{$args.id}" }
5 # The REST endpoint returns "username" and "email" in its
6 # response, and they're mapped directly to fields of
7 # the same name in the GraphQL schema.
8 selection: "id username email"
9 )
10}
11
12type User {
13 id: ID!
14 username: String!
15 email: String!
16}
Renaming fields
Given the following JSON response:
{
"user_id": "1",
"login": "alice",
"email_address": "alice@example.com"
}
You can map a JSON response field to a schema field of a different name using the same syntax as GraphQL aliases:
1type Query {
2 user(id: ID!): User
3 @connect(
4 http: { GET: "https://api.example.com/users/{$args.id}" }
5 selection: """
6 id: user_id
7 username: login
8 email: email_address
9 """
10 )
11}
12
13type User {
14 id: ID!
15 username: String!
16 email: String!
17}
Unwrapping fields
Suppose the JSON response includes nesting that you don't need in your schema:
{
"result": {
"id": "1",
"name": {
"first": "Alice"
},
"profile": {
"username": "alice",
"email": "alice@example.com"
}
}
}
You can "unwrap" fields using the .
prefix:
1type Query {
2 user(id: ID!): User
3 @connect(
4 http: { GET: "https://api.example.com/users/{$args.id}" }
5 selection: """
6 $.result {
7 id
8 name: name.first
9 $.profile {
10 username
11 email
12 }
13 }
14 """
15 )
16}
17
18type User {
19 id: ID!
20 name: String!
21 username: String!
22 email: String!
23}
Using $
when unwrapping
A leading $.
is required when unwrapping a single property.
Without $.
, it is interpreted as mapping the field to create an object.
With $.
, it is interpreted as mapping the value.
For example, given the following JSON:
{ "message": "hello" }
The following selections have the corresponding results:
Selection | Result |
---|---|
message | { "message": "hello" } |
$.message | "hello" |
msg: message | { "msg": "hello" } |
When selecting a path of properties, such as name.first
, the $.
is allowed but not required:
{ "name": { "first": "Alice" } }
Selection | Result |
---|---|
$.name.first | "Alice" |
name.first | "Alice" |
$.name { first } | { "first": "Alice" } |
name { first } | { "name": { "first": "Alice" } } |
The simple form also applies when using value transformations. These are equivalent:
Selection | Result |
---|---|
message->match(["hello", "hi"], ["goodbye", "ciao"]) | { "message": "hi" } |
$.message->match(["hello", "hi"], ["goodbye", "ciao"]) | { "message": "hi" } |
Wrapping fields
You can create nested fields from a flat structure using a variation on the alias syntax. This is especially useful for converting a simple foreign key into an entity reference.
If the foreign keys are in a list, you can use the $
symbol to refer to items in the list.
For example, given the following JSON response:
{
"id": "1",
"company_id": "2",
"address_ids": ["3", "4"]
}
You can create the desired structure using curly braces ({}
) and $
:
1type Query {
2 user(id: ID!): User
3 @connect(
4 http: { GET: "https://api.example.com/users/{$args.id}" }
5 selection: """
6 id
7 company: { id: company_id }
8 addresses: $.address_ids { id: $ }
9 """
10 )
11}
12
13type User {
14 id: ID!
15 company: Company
16 addresses: [Address]
17}
18
19type Company {
20 id: ID!
21}
22
23type Address {
24 id: ID!
25}
Arrays
Mapping arrays happens automatically, so you must ensure that your schema uses list types appropriately.
Given the following JSON response:
{
"results": [
{
"id": "1",
"paymentCards": [
{ "id": "1", "card_type": "Visa" },
{ "id": "2", "card_type": "Mastercard" }
],
"notes": ["note1", "note2"]
}
]
}
You can use the following selection mapping:
1type Query {
2 users: [User] # list 1
3 @connect(
4 http: { GET: "https://api.example.com/users" }
5 selection: """
6 $.results { # list 1
7 id
8 paymentCards { # list 2
9 id
10 type: card_type
11 }
12 notes # list 3
13 }
14 """
15 )
16}
17
18type User {
19 id: ID!
20 paymentCards: [PaymentCard!] # list 2
21 notes: [String!] # list 3
22}
23
24type PaymentCard {
25 id: ID!
26 type: String
27}
Complex nested selection
A complex, nested GraphQL type, User
, maps its fields from a REST endpoint returning multiple nested objects.
See JSON Response
{
"names": {
"username": "alice",
"email": "alice@example.com"
},
"bio": {
"dob": "1999-01-01",
"gender": "Female"
},
"contact": {
"phone": "555-555-5555",
"addresses": [{ "id": "1" }, { "id": "2" }]
},
"payments": [
{
"id": "1",
"card_number": "1234 5678 9012 3456",
"card_type": "Visa",
"exp_date": "12/23",
"default": true
}
],
"cart": [
{
"product": { "id": "1" },
"amt": 2
}
]
}
1type Query {
2 user(id: ID!): User
3 @connect(
4 http: { path: "https://api.example.com/users/{$args.id}" }
5 selection: """
6 id: $args.id
7 $.names {
8 username
9 email
10 }
11 $.bio {
12 dob: birthDate
13 gender
14 }
15 phoneNumber: contact.phone
16 paymentMethods: $.payments {
17 id
18 cardNumber: card_number
19 cardType: card_type
20 expirationDate: exp_date
21 isDefault: default
22 }
23 shippingAddresses: contact.addresses { id }
24 shoppingCart: $.cart {
25 product { id }
26 quantity: amt
27 }
28 """
29 )
30}
31
32type User {
33 id: ID!
34 username: String!
35 email: String!
36 birthDate: Date
37 gender: String
38 phoneNumber: String
39 paymentMethods: [PaymentMethod]
40 shippingAddresses: [ShippingAddress]
41 shoppingCart: [CartItem]
42}
43
44type PaymentMethod {
45 id: ID!
46 cardNumber: String
47 cardType: String
48 expirationDate: String
49 isDefault: Boolean
50}
51
52type ShippingAddress {
53 id: ID!
54}
55
56type CartItem {
57 product: Product
58 quantity: Int
59}