Unions in GraphQL with Mu-Haskell 0.5

Unions in GraphQL with Mu-Haskell 0.5

One of the main features of Mu-Haskell is its support for different protocols. Lately, we’ve concentrated most of our efforts on GraphQL, which we’re now actively using internally at 47 Degrees. As a result, we have released several bug fixes to most packages in the Mu-Haskell ecosystem, and a major version in mu-graphql.

Unions in GraphQL

The schema language in GraphQL supports a concept called union types. In short, being a value of a union means being a value of one of the possible types the union can take. Here’s part of GraphQL schema that defines an Author type with associated Writings that can be Books or Articles:

type Author {
  name: String!
  writings: [Writing!]!
}

union Writing = Book | Article

type Book {
  title: String!
}

type Article {
  title: String!
  magazine: String!
}

As usual with Mu-Haskell, you can directly import this schema into a type-level representation used for checking the consistency of your server. In order to define such a server for the schema, one needs to define two elements: what is the Haskell type representing each GraphQL type (this is called the mapping), and the code that obtains the data for each field (these are called the resolvers). For a more in-depth introduction, you can check our GraphQL documentation.

The mapping works the same way for unions. Most of the time, the corresponding Haskell type is going to be a sum of the different choices. But note that this may not be the case: you can decide to only save a subset or a pointer to the information here, and fill it in in later resolvers.

data WritingMapping
  = ABook BookMapping | AnArticle ArticleMapping

Resolvers for union take a special shape, though. In particular, they need to declare to Mu-Haskell which of the different choices of the union has been taken. This is done using a specialized unionChoice constructor.

union @"Writing" (\case (ABook     x) -> pure $ unionChoice @"Book"    x
                        (AnArticle x) -> pure $ unionChoice @"Article" x)

Most of the time, this union selector is going to be quite “dumb,” like the one above. Right now, this API gives the maximum flexibility at the expense of more writing; we are exploring ways to alleviate this boilerplate to the programmer.

JSON-as-a-type

GraphQL introduces the benefits of static typing in the world of web APIs. However, sometimes an escape hatch is needed, when the information does not have a predefined shape. Since 100% of the times GraphQL is served using JSON, it was just a matter of time until somebody introduced a JSON type. This type is not part of the specification, it’s a so-called custom scalar type.

For example, we may add an optional free-form info field to our Book above by extending the schema as seen below. The scalar JSON line is required by the schema language to signal that a custom scalar is being used.

scalar JSON

...

type Book {
  title: String!
  info: JSON
}

The resolver of that field has to return a Value from the de facto JSON standard library in the Haskell community, namely Aeson. Here’s some code that returns an object with a score field as that info field:

import qualified Data.Aeson as JSON

bookInfo bookId = pure $ do
  bk <- findBook bookId
  pure $ JSON.object ["score" JSON..= bookScore bk]

We found the JSON scalar quite useful in those scenarios in which you cannot exactly pinpoint the shape of data, usually because the model is not yet fixed in stone.

Conclusion

With this release of Mu-Haskell, we get closer to full support of the GraphQL specification. This ties with our goal of giving a first-class experience on each different supported protocol – gRPC, OpenAPI, GraphQL – not simply downgrading to the intersection of the three.

The active development of Mu-Haskell is proudly sponsored by 47 Degrees, a Functional Programming consultancy with a focus on the Scala, Kotlin, Swift, and Haskell programming languages.

Ensure the success of your project

47 Degrees can work with you to help manage the risks of technology evolution, develop a team of top-tier engaged developers, improve productivity, lower maintenance cost, increase hardware utilization, and improve product quality; all while using the best technologies.