Mapping GraphQL Responses
Mapping HTTP responses to GraphQL fields and transforming values
In this guide, you'll learn about:
Why connectors require mapping
Rules for mapping HTTP responses
Examples of common mapping types
How to transform values in your response data
- Just getting start with Apollo Connectors? Try out the quickstart to build your first connector.
- Love developer tooling? Explore Apollo's IDE plugins for GraphQL development, including connectors support.
Mapping overview
Mapping HTTP responses to your GraphQL schema transforms the data returned by your REST APIs into a format that fits your GraphQL API structure. This ensures GraphQL requests can provide consistent, predictable responses to clients, even when working with data from different API sources.
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.
Unique features of selection mapping
In the context of selection mapping, the mapping language has an important, unique feature. When used in selection
, it's assumed that all fields come from the HTTP response body unless otherwise specified. For example, given the following JSON response:
{
"id": 1,
"name": "Lunar Rover Wheels"
}
You can use the following selection to map the id
and name
fields:
type Query {
products: Products
@connect(
source: "ecomm"
http: { GET: "/products" }
selection: "id name"
)
}
type Product {
id: ID!
name: String
}
Multiline syntax
Long selection
strings can be broken up into multiple lines with GraphQL multiline string syntax ("""
):
type Query {
products: Products
@connect(
source: "ecomm"
http: { GET: "/products" }
selection: """
id
name
description
"""
)
}
type Product {
id: ID!
name: String
description: String
}
This is particularly valuable when you have a longer nested selection to map. The following example shows how each line in the selection
translates a JSON response to fields in the GraphQL schema below.
/products/1
{
"id": 1,
"name": "Lunar Rover Wheels",
"variants": [
{
"name": "Standard Wheel",
"price": {
"original": 4999,
"discounts": [],
"final": 4999
},
"specifications": {
"Material": {
"value": "Titanium alloy"
},
"Diameter": {
"value": "50 cm"
}
},
"inventory": {
"quantity": 100,
"sellUnavailable": false
},
"shipping": {
"ship1": {
"weight": 5,
"method": "GROUND",
"estimate": {
"price": 499,
"arrival": 1675804800000
}
},
"ship2": {
"weight": 5,
"method": "AIR",
"estimate": {
"price": 999,
"arrival": 1675790400000
}
}
},
"upc": "0001234567890",
"sku": "RW-001",
"taxable": true,
"variantId": "variant1"
}
]
}
type Query {
product(id: ID!): Product
@connect(
http: { GET: "https://ecommerce.demo-api.apollo.dev/products/{$args.id}" }
selection: """
id # 1
variants { # 2
name # 3
price { # 4
original # 5
final # 6
}
}
"""
)
}
type Product {
id: ID! # 1
variants: [Variant] # 2
}
type Variant {
name: String # 3
price: Price # 4
}
type Price {
original: Int # 5
final: Int # 6
}
$status
variable
In selection
, you also get access to the $status
variable, which isn't available anywhere else. $status
represents the HTTP status code of a response.
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.
type Query {
product(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}" }
selection: "id name description"
)
}
type Product {
id: ID!
name: String!
description: String
reviews: [Review]
@connect(
source: "ecomm"
http: {GET: "/products/{$this.id}/reviews"}
# selection: "$.reviews" ❌ This won't work
selection: """ # ✅ This works
$.reviews {
id
rating
comment
}
"""
)
}
type Review {
id: ID!
rating: Float!
comment: String
}
Even though the reviews
field contains all the information needed from the first connector, you can't map just $.reviews
. You must specify each field—id
, rating
, and comment
—individually. Doing so This enables the query planner to know that the rating
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,
"name": "Lunar Rover Wheels",
"description": "Innovatively designed wheels for lunar rovers, built to endure harsh moon terrain and provide optimal agility. Each wheel is constructed using advanced materials to withstand temperature fluctuations and dust."
}
You can create a basic, flat GraphQL type with fields that map to REST endpoint fields of the same names:
type Query {
product(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}" }
# The REST endpoint returns "id", "name", and "description"
# in its response, and they're mapped directly to fields of
# the same name in the GraphQL schema.
selection: "id name description"
)
}
type Product {
id: ID!
name: String!
description: String!
}
Renaming fields
Given the following JSON response:
{
"product_id": "1",
"title": "Lunar Rover Wheels"
}
You can map a JSON response field to a schema field of a different name using the same syntax as GraphQL aliases.
The desired name (the one present in the schema type
) comes first followed by a colon (:
) and the name of the field in the response: desiredName: original_name
type Query {
product(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}" }
selection: """
id: product_id
name: title
"""
)
}
type User {
id: ID!
name: String!
}
Unwrapping fields
Suppose the JSON response includes nesting that you don't need in your schema:
{
"result": {
"id": "1",
"name": {
"value": "Lunar Rover Wheels"
},
"specifications": {
"material": "Titanium alloy",
"diameter": "50 cm"
}
}
}
You can "unwrap" fields using the .
prefix:
type Query {
product(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}" }
selection: """
$.result {
id
name: name.value
$.specifications {
material
diameter
}
}
"""
)
}
type Product {
id: ID!
name: String!
material: String
diameter: String
}
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:
{ "name": "Lunar Rover Wheels" }
The following selections have the corresponding results:
Selection | Result |
---|---|
name | { "name": "Lunar Rover Wheels" } |
$.name | "Lunar Rover Wheels" |
product_name: name | { "product_name": "Lunar Rover Wheels" } |
When selecting a path of properties, such as name.value
, the $.
is allowed but not required:
{ "name": { "value": "Lunar Rover Wheels" } }
Selection | Result |
---|---|
$.name.value | "Lunar Rover Wheels" |
name.value | "Lunar Rover Wheels" |
$.name { value } | { "value": "Lunar Rover Wheels" } |
name { value } | { "name": { "value": "Lunar Rover Wheels" } } |
The simple form also applies when using value transformations. These are equivalent:
Selection | Result |
---|---|
name->match(["Lunar Rover Wheels", "Wheels"], ["Zero-Gravity Moon Boots", "Boots"]) | { "name": "Wheels" } |
$.name->match(["Lunar Rover Wheels", "Wheels"], ["Zero-Gravity Moon Boots", "Boots"]) | { "name": "Wheels" } |
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",
"brand_id": "2",
"variant_ids": ["3", "4"]
}
You can create the desired structure using curly braces ({}
) and $
:
type Query {
user(id: ID!): Product
@connect(
source: "ecomm"
http: { GET: "/products/{$args.id}" }
selection: """
id
brand: { id: brand_id }
variants: $.variant_ids { id: $ }
"""
)
}
type Product {
id: ID!
brand: Brand
variant: [Variant]
}
type Brand {
id: ID!
}
type Variant {
id: ID!
}
Accessing fields that start with a numerical value
Field names that start with a numerical value must be put in quotes (" "
).
For example, given the following JSON response that includes a specifications.3DModel
field:
{
"id": 1,
"name": "Lunar Rover Wheels",
"specifications": {
"material": "Titanium alloy",
"diameter": "50 cm",
"3DModel": "https://example.com/lunar-rover-wheel-3d"
}
}
You can map the field like so:
1type Query {
2 product(id: ID!): Product
3 @connect(
4 source: "ecomm"
5 http: { GET: "/products/{$args.id}" }
6 selection: """
7 id
8 modelUrl: specifications."3DModel"
9 """
10 )
11}
12
13type Product {
14 id: ID!
15 modelUrl: String
16}
Arrays
Mapping arrays happens automatically, so you must ensure that your schema uses list types appropriately.
Given the following JSON response:
{
"results": [
{
"id": "1",
"variants": [
{ "id": "1", "color": "Silver" },
{ "id": "2", "color": "Platinum" }
],
"reviews": ["Best purchase ever!", "Good value"]
}
]
}
You can use the following selection mapping:
1type Query {
2 products: [Product] # list 1
3 @connect(
4 http: { GET: "https://ecommerce.demo-api.apollo.dev/products" }
5 selection: """
6 $.products { # list 1
7 id
8 variants { # list 2
9 id
10 type: color
11 }
12 reviews # list 3
13 }
14 """
15 )
16}
17
18type User {
19 id: ID!
20 variants: [Variant] # list 2
21 reviews: [String] # list 3
22}
23
24type Variant {
25 id: ID!
26 color: String
27}
Complex nested selection
A complex, nested GraphQL type, Product
, maps its fields from a REST endpoint returning multiple nested objects.
See JSON Response
{
"id": 1,
"name": "Lunar Rover Wheels",
"createdAt": 1675200000000,
"updatedAt": 1675200000000,
"description": "Innovatively designed wheels for lunar rovers, built to endure harsh moon terrain and provide optimal agility. Each wheel is constructed using advanced materials to withstand temperature fluctuations and dust.",
"slug": "lunar-rover-wheels",
"tags": [
{ "tagId": "1", "name": "Instruments" },
{ "tagId": "2", "name": "Space" }
],
"category": "Engineering Components",
"availability": "AVAILABLE",
"variants": [
{
"name": "Standard Wheel",
"price": {
"original": 4999,
"discounts": [],
"final": 4999
},
"specifications": {
"Material": { "value": "Titanium alloy" },
"Diameter": { "value": "50 cm" }
},
"inventory": { "quantity": 100, "sellUnavailable": false },
"variantId": "variant1"
}
]
}
1type Query {
2 product(id: ID!): Product
3 @connect(
4 http: { path: "https://ecommerce.demo-api.apollo.dev/products/{$args.id}" }
5 selection: """
6 id
7 name
8 description
9 availability
10 createdAt
11 updatedAt
12 tags: $.tags {
13 id: tagId
14 name
15 }
16 variants: $.variants {
17 name
18 taxable
19 price {
20 original
21 final
22 }
23 specifications: $.specifications {
24 material: Material.value
25 diameter: Diameter.value
26 }
27 inventory {
28 quantity
29 sellUnavailable
30 }
31 }
32 """
33 )
34}
35
36type Product {
37 id: ID!
38 name: String!
39 createdAt: Float
40 updatedAt: Float
41 description: String!
42 availability: String!
43 tags: [Tag]
44 variants: [Variant]
45}
46
47type Tag {
48 id: ID!
49 name: String!
50}
51
52type Variant {
53 name: String!
54 price: Price
55 specifications: Specifications
56 inventory: Inventory
57}
58
59type Price {
60 original: Int
61 final: Int
62}
63
64type Specifications {
65 material: String
66 diameter: String
67}
68
69type Inventory {
70 quantity: Int
71 sellUnavailable: Boolean
72}
Transforming values
As part of your selection mapping, you can transform values in your response using the mapping language's methods. Methods use the ->method
syntax. See the mapping language methods reference for the complete list of available methods.
Enum value mapping
Enum value mapping is helpful when your response data uses values that don't match the exact format or case sensitivity required by your GraphQL schema.
The example below uses the ->match
method to transform status
values from active
to ACTIVE
and not active
to INACTIVE
.
status: status->match(
["active", "ACTIVE"],
["not active", "INACTIVE"],
[@, "UNKNOWN"] # fallback — the value always matches `@`
)
Using the above transformation on the following response data yields the following results:
{
"status": "active"
}
{
"status": "ACTIVE"
}
{
"status": "none of the above"
}
{
"status": "UNKNOWN"
}
If a match isn't found, the result will be omitted and the field will be null
if nullable or result in a validation error if non-nullable. If you want to avoid this, you can use the @
variable to provide a fallback, as shown in the example above.
Convert a map into a list of key-value pairs
Converting a map into a list of key-value pairs is particularly useful when you need to work with data in a more structured or iterable format. For example, in a frontend application, you might want to render a list of items, such as color names and their corresponding hex codes.
The example below uses the ->entries
method to convert a map of color names and hex codes into a list of objects. You can use the following selection mapping snippet:
colors: colors->entries
To transform response data like this:
{
"colors": {
"red": "#ff0000",
"green": "#00ff00",
"blue": "#0000ff"
}
}
{
"colors": [
{ "key": "red", "value": "#ff0000" },
{ "key": "green", "value": "#00ff00" },
{ "key": "blue", "value": "#0000ff" }
]
}
To use different names for keys and values, select the fields with aliases:
colors: colors->entries {
name: key
hex: value
}
{
"colors": {
"red": "#ff0000",
"green": "#00ff00",
"blue": "#0000ff"
}
}
{
"colors": [
{ "name": "red", "hex": "#ff0000" },
{ "name": "green", "hex": "#00ff00" },
{ "name": "blue", "hex": "#0000ff" }
]
}
List management
Various methods, including ->first
, ->last
, ->slice
, and ->size
, let you transform lists.
For example, to transform a list into its first value, use ->first
like so:
color->first
{
"color": [
"red",
"green",
"blue"
]
}
{
"color": "red"
}
To wrap a single item in a list, you can use a literal list and select the property:
$([$.color])
{
"color": "red"
}
{
"color": ["red"]
}
Additional resources
Refer to the mapping language reference for a complete overview of mapping syntax and usage.