3
\$\begingroup\$

I have these types:

data PizzaType = Cheese | Pepperoni | Anchovy

data PizzaOrder = PizzaOrder
    { p_type :: PizzaType
    , p_specialInstructions :: Text
    }

I want to use Aeson to create PizzaOrder instances from this JSON structure:

{
    "type": "pepperoni",
    "specialInstructions": "please cut into nine slices"
}

I have this function to convert from a Text to a PizzaType:

convertPizzaType :: Text -> Maybe PizzaType
convertPizzaType "cheese" = Just Cheese
convertPizzaType "pepperoni" = Just Pepperoni
convertPizzaType "anchovy" = Just Anchovy
convertPizzaType _ = Nothing

(The actual code to convert from a Text to my custom data type is much more involved, but it’s not the main point of my question.) In order to use my convertPizzaType function in the JSON parsing process, I wrote this general function

parseMaybe :: String -> (a -> Maybe b) -> Parser a -> Parser b
parseMaybe err f p = do
    result <- p
    case f result of
      Nothing -> fail err
      Just x -> pure x

which I use like this:

instance FromJSON PizzaOrder where
    parseJSON (Object v) = PizzaOrder <$>
        parseMaybe "Unknown pizza type" convertPizzaType (v .: "type") <*>
        v .: "specialInstructions"
    parseJSON _  = mzero

My question involves my parseMaybe function. It takes the raw input from Aeson, runs the given conversion function, and aborts the parsing if Nothing was returned. This seems to work but I have the sneaking suspicion that I’m reinventing a wheel. Is this a reasonable way to embed my convertPizzaType function in a Parser or should I be doing something simpler?

Complete runnable example

#!/usr/bin/env stack
-- stack --resolver lts-3.14 --install-ghc runghc --package aeson

{-# LANGUAGE OverloadedStrings #-}

import Control.Monad (mzero)
import Data.Aeson
import Data.Aeson.Types (Parser)
import Data.Text (Text)

data PizzaType = Cheese | Pepperoni | Anchovy deriving (Show)

data PizzaOrder = PizzaOrder
    { p_type :: PizzaType
    , p_specialInstructions :: Text
    }

instance FromJSON PizzaOrder where
    parseJSON (Object v) = PizzaOrder <$>
        parseMaybe "Unknown pizza type" convertPizzaType (v .: "type") <*>
        v .: "specialInstructions"
    parseJSON _  = mzero

parseMaybe :: String -> (a -> Maybe b) -> Parser a -> Parser b
parseMaybe err f p = do
    result <- p
    case f result of
      Nothing -> fail err
      Just x -> pure x

convertPizzaType :: Text -> Maybe PizzaType
convertPizzaType "cheese" = Just Cheese
convertPizzaType "pepperoni" = Just Pepperoni
convertPizzaType "anchovy" = Just Anchovy
convertPizzaType _ = Nothing

main :: IO ()
main = do
    let example1 = "{\"type\": \"pepperoni\", \"specialInstructions\": \"none\"}"
        parsedExample1 = decode example1 :: Maybe PizzaOrder
    case parsedExample1 of
      Nothing -> putStrLn "Example 1 couldn't be parsed"
      Just o -> putStrLn $ "The type of example 1 was " ++ (show $ p_type o)

    let example2 = "{\"type\": \"broccoli\", \"specialInstructions\": \"EXTRA BROCCOLI\"}"
        parsedExample2 = decode example2 :: Maybe PizzaOrder
    case parsedExample2 of
      Nothing -> putStrLn "Example 2 couldn't be parsed"
      Just o -> putStrLn $ "The type of example 2 was " ++ (show $ p_type o)
\$\endgroup\$
3
\$\begingroup\$

Modern functional programming and pizza are best friends, so I commend the usage of Haskell here.

As PizzaType is already its own type, it's probably easiest to implement a FromJSON PizzaType instance. We know Parser is a monad, so we can use >>= to sequence parser computations together.

instance FromJSON PizzaType where
  parseJSON (String s) =
    maybe err pure (convertPizzaType s)
    where
      err = (fail . unpack) ("PizzaType: unknown type " <> s)
  parseJSON invalid =
    typeMismatch "PizzaType" invalid

instance FromJSON PizzaOrder where
  parseJSON (Object v) =
    PizzaOrder <$> (v .: "type" >>= parseJSON) <*> v .: "specialInstructions"
  parseJSON invalid =
    typeMismatch "PizzaOrder" invalid

I also recommend using two helpers:

  • typeMismatch to make the Aeson errors a little better. It's easy, with large JSON input, to quickly run into error hell where you have no idea where an error is coming from. Best to be specific early on.

  • maybe to convert values from Maybe a to Parser a.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.