04 Jan 2026

Validation of data in a servant server

I've been playing around with adding more validation of data received by an HTTP endpoint in a servant server. Defining a type with a FromJSON instance is very easy, just derive a Generic instance and it just works. Here's a simple example

data Person = Person
    { name :: Text
    , age :: Int
    , occupation :: Occupation
    }
    deriving (Generic, Show)
    deriving (FromJSON, ToJSON) via (Generically Person)

data Occupation = UnderAge | Student | Unemployed | SelfEmployed | Retired | Occupation Text
    deriving (Eq, Generic, Ord, Show)
    deriving (FromJSON, ToJSON) via (Generically Occupation)

However, the validation is rather limited, basically it's just checking that each field is present and of the correct type. For the type above I'd like to enforce some constraints for the combination of age and occupation.

The steps I thought of are

  1. Hide the default constructor and define a smart one. (This is the standard suggestion for placing extra constraints values.)
  2. Manually define the FromJSON instance using the Generic instance to limit the amount of code and the smart constructor.

The smart constructor

I give the constructor the result type Either String Person to make sure it can both be usable in code and when defining parseJSON.

mkPerson :: Text -> Int -> Occupation -> Either String Person
mkPerson name age occupation = do
    guardE mustBeUnderAge
    guardE notUnderAge
    guardE tooOldToBeStudent
    guardE mustBeRetired
    pure $ Person name age occupation
  where
    guardE (pred, err) = when pred $ Left err
    mustBeUnderAge = (age < 8 && occupation > UnderAge, "too young for occupation")
    notUnderAge = (age > 15 && occupation == UnderAge, "too old to be under age")
    tooOldToBeStudent = (age > 45 && occupation == Student, "too old to be a student")
    mustBeRetired = (age > 65 && occupation /= Retired, "too old to not be retired")

Here I'm making use of Either e being a Monad and use when to apply the constraints and ensure the reason for failure is given to the caller.

The FromJSON instance

When defining the instance I take advantage of the Generic instance to make the implementation short and simple.

instance FromJSON Person where
    parseJSON v = do
        Person{name, age, occupation} <- genericParseJSON defaultOptions v
        either fail pure $ mkPerson name age occupation

If there are many more fields in the type I'd consider using RecordWildCards.

Conclusion

No, it's nothing ground-breaking but I think it's a fairly nice example of how things can fit together in Haskell.

Tags: haskell servant
Comment here.