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
- Hide the default constructor and define a smart one. (This is the standard suggestion for placing extra constraints values.)
- Manually define the
FromJSONinstance using theGenericinstance 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.