Mapping GraphQL Responses

Mapping HTTP responses to GraphQL fields and transforming values


preview
Apollo Connectors are currently in public preview. You need an Apollo account with a GraphOS Trial or Enterprise plan to get started. Update to router v.2.0.0-preview.6 and federation v.2.10.0-preview.6 to access the latest features.

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

tip
Just getting start with Apollo Connectors? Try out the quickstart to build your first connector.

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.

GraphQL
Example connector with selection
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.

tip
Long selection strings can be broken up into multiple lines with GraphQL multiline string syntax ("""):

GraphQL
Example: multiline selection
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:

connectors
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.

GraphQL
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:

JSON
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:

GraphQL
Example: basic selection
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:

JSON
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:

GraphQL
Example: renaming fields
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:

JSON
JSON Response
{
  "result": {
    "id": "1",
    "name": {
      "first": "Alice"
    },
    "profile": {
      "username": "alice",
      "email": "alice@example.com"
    }
  }
}

You can "unwrap" fields using the . prefix:

GraphQL
Example: unwrapping
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:

JSON
{ "message": "hello" }

The following selections have the corresponding results:

SelectionResult
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:

JSON
{ "name": { "first": "Alice" } }
SelectionResult
$.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:

SelectionResult
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:

JSON
JSON Response
{
  "id": "1",
  "company_id": "2",
  "address_ids": ["3", "4"]
}

You can create the desired structure using curly braces ({}) and $:

GraphQL
Example: wrapping fields
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:

JSON
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:

GraphQL
Example: wrapping fields
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
JSON
{
  "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
    }
  ]
}
GraphQL
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}

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.

GraphQL
Selection mapping snippet
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:

JSON
Response data
{
  "status": "active"
}
JSON
Result
{
  "status": "ACTIVE"
}
JSON
Response data
{
  "status": "none of the above"
}
JSON
Result
{
  "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:

GraphQL
Selection mapping snippet
colors: colors->entries

To transform response data like this:

JSON
Response data
{
  "colors": {
    "red": "#ff0000",
    "green": "#00ff00",
    "blue": "#0000ff"
  }
}
JSON
Result
{
  "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:

GraphQL
Selection mapping snippet
colors: colors->entries {
  name: key
  hex: value
}
JSON
Response data
{
  "colors": {
    "red": "#ff0000",
    "green": "#00ff00",
    "blue": "#0000ff"
  }
}
JSON
Result
{
  "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:

GraphQL
Selection mapping snippet
color->first
JSON
Response data
{
  "color": [
    "red",
    "green",
    "blue"
  ]
}
JSON
Result
{
  "color": "red"
}

To wrap a single item in a list, you can use a literal list and select the property:

GraphQL
Selection mapping snippet
$([$.color])
JSON
Response data
{
  "color": "red"
}
JSON
Result
{
  "color": ["red"]
}
Feedback

Forums