How to use UUID values with Persistent and Yesod

Some people find it trickier to store UUID values in their database with Persistent or to use UUID values in a Yesod web application than is really necessary. Here I’ll share some code from my work that demonstrates some patterns in applications that use Persistent or Yesod which should make it easier.

The context for this post can be found in these two links:

Replying to: Jezen Thomas writing about using UUIDs in Yesod
Prior art: Michael Xavier on UUID columns in Persistent

Alternate title: Same as the original, but with: “and no Control.Lens needed” tacked on.

This code is adapted from stuff we’ve written at work.

Persistent / UUID integration

Instances

Radioactive dumping ground for orphan instances. Adding the instances makes Persistent understand how to serialize and deserialize the UUID type. The orphans can be avoided if you use a newtype.

-- Note we're taking advantage of
-- PostgreSQL understanding UUID values,
-- thus "PersistDbSpecific"
instance PersistField UUID where
  toPersistValue u = PersistDbSpecific . B8.pack . UUID.toString $ u
  fromPersistValue (PersistDbSpecific t) =
    case UUID.fromString $ B8.unpack t of
      Just x -> Right x
      Nothing -> Left "Invalid UUID"
  fromPersistValue _ = Left "Not PersistDBSpecific"

instance PersistFieldSql UUID where
  sqlType _ = SqlOther "uuid"

Models

This is where we actually use the UUID type in our models.

module MyCompany.DB.Models where

share
  [mkPersist sqlSettings,mkMigrate "migration"]
  [persistLowerCase|
User json sql=users
  email                  Email       sqltype=text
  UniqUserEmail        email
  uuid                   UUID        sqltype=uuid   default=uuid_generate_v4()
  UniqUserUuid         uuid
  deriving Show Read Eq Typeable
|]

We use the default JSON representation generated so that the format is predictable for the datatypes. I was a little queasy with this initially and it does mean we have to watch what happens to Aeson, but I believe net-net it reduces defects that reach production.

Yesod PathPiece integration

PathPiece is the typeclass Yesod uses to deserialize Text data into a more structured type, so that something like the following:

!/#Subdomain/#NumberedSlug    SomeRouteR  GET

could work. Here Subdomain and NumberedSlug are domain-specific types we made to represent a concept in our application in a type-safe manner. PathPiece often goes unnoticed by people new to Yesod, but it’s good to understand. For a super simple example:

newtype Subdomain = Subdomain Text
  deriving (Eq, Show, Read)

instance PathPiece Subdomain where
  toPathPiece (Subdomain t) = t
  fromPathPiece = Just . Subdomain

The PathPiece class itself isn’t terribly complicated or interesting:

-- https://hackage.haskell.org/package/path-pieces-0.2.1/docs/Web-PathPieces.html

-- S for "Strict"
class PathPiece s where
    fromPathPiece :: S.Text -> Maybe s
    toPathPiece :: s -> S.Text

To address the original post’s code, I wouldn’t have written that myself. I generally keep DB/IO stuff apart from things like forms. Partly this is because our web app repository is separate from our domain types / DB stuff repo, which sort of forces us to refactor things we might need to do more than once, or in a context that isn’t a web app. The use of applicative style and the double-lifting was not idiomatic.

Alternate title rejected for being too snarky: I told the doctor it hurts when I move my arm a certain way. The doctor told me to stop moving my arm like that.

I know this site is a bit of a disaster zone, but if you like my writing or think you could learn something useful from me, please take a look at the book I've been writing with my coauthor Julie. There's a free sample available too!