HiveBrain v1.2.0
Get Started
← Back to all entries
snippetMinor

Embedding a Text -> Maybe MyDataType function in a Parser to parse JSON into a custom data type

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
mydatatypeparserintotextfunctionjsonparsetypecustomembedding

Problem

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  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_specialInstruction

Solution

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.

Code Snippets

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

Context

StackExchange Code Review Q#115190, answer score: 3

Revisions (0)

No revisions yet.