haskell / aeson

A fast Haskell JSON library
Other
1.26k stars 320 forks source link

Generic all nullary to string tag with default option? #821

Closed noughtmare closed 3 years ago

noughtmare commented 3 years ago

Consider this data type and parsing code:

data Category = WebDev | ObjectOriented | Python | ProgLanguage | Misc deriving (Show, Eq)

parseCategory :: Value -> Category
parseCategory (String "Object Oriented") = ObjectOriented
parseCategory (String "Python") = Python
parseCategory (String "Web Development") = WebDev
parseCategory (String "Programming Language") = ProgLanguage
parseCategory _ = Misc

If you want to derive this parsing code automatically then you need to edit some of the fields and use customOptions:

customOptions = defaultOptions
                { constructorTagModifier = intercalate " " . splitCamel
                }

splitCamel :: String -> [String]
splitCamel = finish . foldr step (True, [""]) where
  finish (_, "" : xs) = xs
  finish (_, xs) = xs

  step x ~(b', ys') = (b, stepList ys')
    where
      b = isUpper x
      stepList
        | b && not b' = newWord . newLetter x
        | not b && b' = newLetter x . newWord
        | otherwise = newLetter x

  newWord ("" : xs) = "" : xs
  newWord (x : xs) = "" : (x : xs)

  newLetter c (x : xs) = (c : x) : xs

data Category
  = WebDevelopment -- more consistent
  | ObjectOriented
  | Python
  | ProgrammingLanguage -- more consistent
  deriving (Show, Eq, Generic)

instance FromJSON Category where
    parseJSON = genericParseJSON customOptions

But this doesn't catch the Misc case. I have tried wrapping the Category up in a Maybe hoping that a failure to parse would result in Nothing, e.g. decode "\"ABC\"" :: Maybe (Maybe Category) expecting Just Nothing, but it returns Nothing unfortunately. Is there a way to do this or could such functionality be added?

This came up in the Monthly Hask Anything thread on reddit.

phadej commented 3 years ago

I think writing manual instance is a way to go here.

I have written some code for enumeration types which use with DerivingVia may look like

data Category
  = WebDevelopment -- more consistent
  | ObjectOriented
  | Python
  | ProgrammingLanguage -- more consistent
  deriving (FromJSON, ToJSON) via NamedEnum '["Web development", "Object Oriented", ...]

In general, I'm open to embracing DerivingVia instead of trying to make the single generics deriving be capable of cater for all imaginable use cases.

However that doesn't have catch-all case. My experience suggests it is better to have strict enumeration, and then maybe a custom wrapper (if you don't want to use Maybe). e.g. Cabal have KnownExtension and Extension (and few other similar pairs).

noughtmare commented 3 years ago

@phadej thanks for the suggestion. That DerivingVia approach is also better because then the constructor names don't have to be changed.