Selection Mapping
How to map API endpoint responses to GraphQL schemas with Apollo Connectors
Overview
Use a generalized selection mapping syntax with the @connect
directive to transform JSON response payloads to match fields in your GraphQL subgraph schemas. Use this selection syntax with the selection
argument.
The selection
mapping determines how the endpoint's JSON response corresponds to your GraphQL schema. To use a field in the response, it must be listed in the selection string. This includes nested types.
1type Query {
2 products: Products
3 @connect(
4 http: { GET: "https://api.example.com/products" }
5 selection: """
6 id # 1
7 variation { # 2
8 size { # 3
9 width # 4
10 height # 5
11 }
12 color # 6
13 }
14 """
15 )
16}
17
18type Product {
19 id: ID! # 1
20 variation: Variation # 2
21}
22
23type Variation {
24 size: Dimension # 3
25 color: String # 6
26}
27
28type Dimension {
29 width: Int # 4
30 height: Int # 5
31}
Examples
Basic direct selection
A basic, flat GraphQL type with fields that map to REST endpoint fields of the same names.
1{
2 "id": "1",
3 "username": "alice",
4 "email": "alice@example.com"
5}
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
Mapping a JSON response field to a schema field of a different name uses the same syntax as GraphQL aliases.
1{
2 "user_id": "1",
3 "login": "alice",
4 "email_address": "alice@example.com"
5}
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
When the JSON response includes nesting that you don't need in your schema, you can "unwrap" fields using the .
prefix.
1{
2 "result": {
3 "id": "1",
4 "name": {
5 "first": "Alice"
6 },
7 "profile": {
8 "username": "alice",
9 "email": "alice@example.com"
10 }
11 }
12}
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}
A leading $.
is required when unwrapping a single property. Without $.
, it is interpreted as selecting the field to make an object. With $.
, it is interpreted as selecting the value.
1{ "message": "hello" }
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:
1{ "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.
1{
2 "id": "1",
3 "company_id": "2",
4 "address_ids": ["3", "4"]
5}
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.
1{
2 "results": [
3 {
4 "id": "1",
5 "paymentCards": [
6 { "id": "1", "card_type": "Visa" },
7 { "id": "2", "card_type": "Mastercard" }
8 ],
9 "notes": ["note1", "note2"]
10 }
11 ]
12}
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.
JSON Response
1{
2 "names": {
3 "username": "alice",
4 "email": "alice@example.com"
5 },
6 "bio": {
7 "dob": "1999-01-01",
8 "gender": "Female"
9 },
10 "contact": {
11 "phone": "555-555-5555",
12 "addresses": [{ "id": "1" }, { "id": "2" }]
13 },
14 "payments": [
15 {
16 "id": "1",
17 "card_number": "1234 5678 9012 3456",
18 "card_type": "Visa",
19 "exp_date": "12/23",
20 "default": true
21 }
22 ],
23 "cart": [
24 {
25 "product": { "id": "1" },
26 "amt": 2
27 }
28 ]
29}
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}
Requests
Using selection mapping to create request bodies
Selection mapping can also be used to create request bodies for POST, PUT, and PATCH requests.
You can "unwrap" fields from field arguments using the $args
variable.
1type Mutation {
2 createUser(input: CreateUserInput!): User
3 @connect(
4 http: {
5 POST: "https://api.example.com/users"
6 body: """
7 $args.input {
8 username
9 email
10 password
11 }
12 """
13 }
14 selection: "id username email"
15 )
16}
17
18type CreateUserInput {
19 username: String!
20 email: String!
21 password: String!
22}
1{
2 "username": "alice",
3 "email": "alice@example.com",
4 "password": "password123"
5}
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
}
"""
Literal values
You can use literal values in the selection string with $()
. This is useful for adding constant values to the request body.
1body: """
2hello: $("world")
3theAnswer: $(42)
4isTrue: $(true)
5anObject: $({ key: "value" })
6aList: $([1, 2, 3])
7"""
To avoid using the $()
wrapper repeatedly, you can also wrap the whole object with $()
:
1body: """
2$({
3 hello: "world",
4 theAnswer: 42,
5 isTrue: true,
6 anObject: { key: "value" },
7 aList: [1, 2, 3],
8})
9"""
Note that commas are required between the properties of the object literal.
Inside the $()
expression, you can use any JSON literal: numbers, strings, booleans, arrays, objects, and null
. You can also use variables like $args
, $this
, and selections from the response.
1selection: """
2names: $([ # a list field like `names: [String]`
3 $args.input.name, # a variable
4 results->first.name # a selection
5])
6"""
Result
1{
2 "$args": {
3 "input": {
4 "name": "Alice"
5 }
6 }
7}
1{
2 "results": [
3 { "name": "Bob" }
4 { "name": "Charlie" }
5 ]
6}
1{ "names": ["Alice", "Bob"] }
selection
argument of the @connect
directive.- Literal values can't be mapped to a field that returns a nested object or list of nested objects.
- Scalar literal values (strings, numbers, booleans) and lists of scalars are allowed.
- Literal objects and lists and lists of objects are allowed only if they're mapped to a custom scalar field.
- All literal values in the
body
argument are always allowed.
1type Mutation {
2 createPost(input: CreatePostInput!): PostPayload
3 @connect(
4 http: {
5 POST: "https://api.example.com/posts"
6 body: "$({ id: 1, title: "Hello, world!" })" # ✅
7 }
8 selection: """
9 success: $(true) # ✅
10 post: $({ id: 1, title: "Hello, world!" }) # ❌
11 metadata: $({ key: "value" }) # ✅
12 """
13 )
14
15type PostPayload {
16 success: Boolean
17 post: Post # ⚠️ Object type
18 metadata: JSON
19}
20
21scalar JSON
Form URL encoding
By adding a Content-Type
header of exactly application/x-www-form-urlencoded
, GraphOS Router will encode the request body as a form URL encoded string.
1type Mutation {
2 createPost(input: CreatePostInput!): Post
3 @connect(
4 http: { POST: "https://api.example.com/posts" }
5 headers: { "Content-Type": "application/x-www-form-urlencoded" }
6 selection: """
7 $args.input {
8 title
9 content
10 }
11 """
12 )
13}
The request body is first mapped to an object:
1{
2 "title": "Hello, world!",
3 "content": "This is a post."
4}
Then, it is encoded as a form URL encoded string:
1title=Hello%2C+world%21&content=This+is+a+post.
content-type
header on the @source
directive and all connectors with request bodies will use that content type and encoding (unless overridden headers
defined on a connector).1extend schema
2 @source(
3 name: "v1"
4 http: {
5 baseURL: "https://api.example.com"
6 headers: [
7 { "content-type": "application/x-www-form-urlencoded" }
8 ]
9 }
10 )
Form URL encoding details
- List values are indexed starting from 0 using the
list[0]=value
syntax. - Nested objects use the
parent[child]=value
syntax. - Spaces are encoded as
+
.
1type Mutation {
2 example(input: ExampleInput!): Example
3 @connect(
4 http: { POST: "/example" }
5 headers: { "content-type": "application/x-www-form-urlencoded" }
6 selection: """
7 $args.input {
8 name
9 tags
10 addresses {
11 street
12 city
13 state
14 zip
15 }
16 }
17 """
18 )
19}
20
21input ExampleInput {
22 name: String!
23 tags: [String!]
24 addresses: [AddressInput!]
25}
26
27input AddressInput {
28 street: String!
29 city: String!
30 state: String!
31 zip: String!
32}
1name=Example
2&tags[0]=tag1
3&tags[1]=tag2
4&addresses[0][street]=123+Main+St
5&addresses[0][city]=Anytown
6&addresses[0][state]=CA
7&addresses[0][zip]=12345
8&addresses[1][street]=456+Elm+St
9&addresses[1][city]=Othertown
10&addresses[1][state]=NY
11&addresses[1][zip]=54321
Variables
The selection string can use variables to refer to GraphQL arguments, sibling fields, or configuration values. These are the same variables available in URL templates.
Argument | Description |
---|---|
$args | The GraphQL arguments passed to the field. |
$this | The parent object of the current GraphQL field. Used to refer to sibling GraphQL fields. |
$config | The configuration passed to GraphOS Router. |
$ | The value enclosed by the parent {...} selection, or the root value at the top level. |
@ | The current value, which may differ from $ along a nested path. |
$this and query planning
$this
creates a dependency between the connector and fields on the parent object. This implies that another connector or another subgraph can provide this field.1type Product {
2 id: ID!
3 reviews: [Review]
4 # Some other connector or subgraph provides the `id` value.
5 @connect(
6 http: { GET: "/products/{$this.id}/reviews" }
7 selection: "id rating comment"
8 )
9}
@requires
directive in Apollo Federation. This connector definition:1type Product {
2 id: ID!
3 weight: Int
4 shippingCost: Int
5 @connect(http: { GET: "/shipping?weight=${this.weight}" }, selection: "$")
6}
1type Product @key(fields: "id") {
2 id: ID!
3 weight: Int @external
4 shippingCost: Int @requires(fields: "weight")
5}
@requires
to create computed fields using REST APIs.Value transformations
You can transform values in the response mapping using the ->method
syntax.
The arguments to a method are JSON literals (numbers, strings, booleans, arrays, objects, and null) or variables ($
, $args
, $this
, or $config
).
In addition, the variable @
refers to the value being transformed, which is
usually the same as $
, but may differ from $
along a nested path. For
example, in the value->echo({ wrapped: @ })
example below, @
refers to the
value of value
, while $
refers to the object containing value
.
Additionally, the colors->map({ name: @ })
method binds @
to each element in
the $.colors
list, while $
remains unchanged.
The following methods are available:
Method | Description | Example |
---|---|---|
echo | Evaluates and returns its first argument | wrappedValue: value->echo({ wrapped: @ }) |
map | Maps a list of values to a new list of values | colors: colors->map({ name: @ }) |
match | Replaces a value with a new value if it match another value | status: status->match([1, "one"], [2, "two"], [@, "other"]) |
first | Returns the first value in a list | firstColor: colors->first |
last | Returns the last value in a list | lastColor: colors->last |
slice | Returns a slice of a list | firstTwoColors: colors->slice(0, 2) |
size | Returns the length of a list | colorCount: colors->size |
entries | Returns a list of key-value pairs | keyValuePairs: colors->entries |
Value transformation recipes
Enum value mapping
1selection: """
2status: status->match(
3 ["active", "ACTIVE"],
4 ["not active", "INACTIVE"],
5 [@, "UNKNOWN"] # fallback — the value always matches `@`
6)
7"""
null
if nullable or result in a validation error if non-nullable.Convert a map into a list of key value pairs
1{
2 "colors": {
3 "red": "#ff0000",
4 "green": "#00ff00",
5 "blue": "#0000ff"
6 }
7}
1selection: """
2colors: colors->entries
3"""
1{
2 "colors": [
3 { "key": "red", "value": "#ff0000" },
4 { "key": "green", "value": "#00ff00" },
5 { "key": "blue", "value": "#0000ff" }
6 ]
7}
1selection: """
2colors: colors->entries {
3 name: key
4 hex: value
5}
6"""
1{
2 "colors": [
3 { "name": "red", "hex": "#ff0000" },
4 { "name": "green", "hex": "#00ff00" },
5 { "name": "blue", "hex": "#0000ff" }
6 ]
7}
List management
1selection: """
2results->first
3"""
1selection: """
2$([$.result])
3"""