RFC X / Rejected

Map type

Opened on2021-09-29
Closed on2021-11-09

At a glance

Spec PR description

This is an RFC for a new "Map" type to be added to GraphQL.

I acknowledge issue #101, that has 79 comments and 150+ 👍 votes. @leebyron locked the issue with the comment

If someone feels strongly that this concept deserves first-class support in GraphQL, I suggest following the RFC procedure to take this from a general suggestion to an actual proposal.

This is that proposal.

Problem statement

This proposal aims to keep in mind "The Guiding Principles" laid out in the CONTRIBUTING.md.

Currently, GraphQL doesn't offer a way to return a Map/Dictionary response.

A workaround is to return a key/value pair response as suggested in https://stackoverflow.com/questions/56705157/best-way-to-define-a-map-object-in-graphql-schema

type ArticleMapTuple {
     key: String!
     value: Article!
}

type Article {
  name: String!
}

response

[
  {
    "key": "foo1",
    "value": {name: "Foo1"}
  },
  {
    "key": "foo2",
    "value": {name: "Foo2"}
  },
  {
    "key": "foo3",
    "value": {name: "Foo3"}
  },
]

The problem is searching for the key "foo3" in the list requires traversing through the list. The alternative is to process the response into a local object via Object.fromEntries and then use it for fast lookups.

Maps/Dictionaries are core data types in most languages. The json spec supports objects with key: value pairs. By having support for Maps, GraphQL clients can make effient key:value lookups on responses.

This proposal introduces field: { Type } syntax to specify Maps. Similar to existing field: [ List ] syntax.

The primary motivation in this proposal is the idea that Maps are Lists with ID! (non-null string) keys, and should behave similar to Lists.

Most relational databases have tables with schemas in the format:

type SomeEntity {
  id: ID!
  field1: String!
  field2: Int!
}

Having the response with IDs as keys gives GraphQL consumers/clients the ability for O(1) map lookups instead of O(n) list lookups.

{
 "idAbc": {field1: "foo", field2: 123}
}

The other argument is that in many instances, GraphQL sits on top of an existing REST-ful api which returns responses with map responses. A real-world example is algolia.

Algolia indexes map fields for very fast facet lookups. e.g.

{
 id: "123"
 name: "K95 Face Shield 24 PK",
 stockByLocation: {
   "seattle": 30,
   "portland": 40,
   "miami": 30,
   "st_louis": 10,
   ...
 }
}

To implement a GraphQL api over algolia, it would require changing the shape of stockByLocation response. By having GraphQL as schema enforcer, Map type would open a lot more possibilities of GraphQL adoption.

The schema for above response would be:


type InventoryItem {
  id: ID!
  name: String!
  stockByLocations: { Int! }!
}

List type

Currently, the List type is the only unbounded type in GraphQL.

SDL

type Query {
  users: [User!]!
}

type User {
  id: ID!
  firstName: String!
  lastName: String!
}

query:

{
  users {
    id
    firstName
    lastName
  }
}

response:

{
  "users": [
    {"id": "foo", "firstName": "Foo", "lastName": "Bar"},
    {"id": "hello", "firstName": "Hello", "lastName": "World"}
  ]
}

Notice how the query didn't specify [] to specify a list response. Based on the type declaration users: [User!]!, only the fields of the List's value type are specified.

{
  users [{
    id
    firstName
    lastName
  }]
}

^ NOTE: this is an invalid gql query.

The response can return any number of items in the list. GraphQL doesn't control what will be returned at the 0th index of the list, or the 1st index. This is upto the GraphQL service to determine.

A list can be seen a map with incremental numeric keys. It supports fast lookups at an index.

[
    0: {"id": "foo", "firstName": "Foo", "lastName": "Bar"},
    1: {"id": "hello", "firstName": "Hello", "lastName": "World"},
    2: {"id": "jsmith", "firstName": "John", "lastName": "Smith"}
]

Map type

Following the principle of "Maps are Lists with string keys, and should behave simiar to Lists."

Note: The value type will still need to be explicitly specified. This is not an escape hatch for Any type.

SDL

type Query {
  users: {User!}!
}

type User {
  id: ID!
  firstName: String!
  lastName: String!
}

query:

{
  users {
    firstName
    lastName
  }
}

response:

{
  "users": {
    "foo": {"firstName": "Foo", "lastName": "Bar"},
    "hello": {"firstName": "Hello", "lastName": "World"}
  }
}

Q: Why non-null string keys only?

A: Because grapqhl responses are json, and json only supports string keys.

Alternative syntax is field: {ID!: Type}, however that would indicate that GraphQL may support other key types like Ints. I'd love for users to fall into the pit of success, so feel the semantics should be simple. Only string key types. Less is more.

field: { Type } for Maps, field: [ Type ] for Lists. The non-null versions being. field: { Type! }! and field: [ Type! ]!.

--

Q: What about nested Maps?

A: Nested lists work e.g. field: [[ Type ]], therefore, nested maps should also work in a similar fashion i.e. field: {{ Type }}. The difference is that there would be no automatic coercion. If the shape of response doesn't match then there is a type error.

Timeline