Mapping GraphQL Responses

Mapping HTTP responses to GraphQL fields


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.4 and federation v.2.10.0-preview.4 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

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}
Feedback

Forums