Aeson: parsing dynamic keys as type field

2019-05-04 00:37发布

Let's say there is a JSON like:

{
  "bob_id" : {
    "name": "bob",
    "age" : 20
  },
  "jack_id" : {
    "name": "jack",
    "age" : 25
  }
}

Is it possible to parse it to [Person] with Person defined like below?

data Person = Person {
   id   :: Text
  ,name :: Text
  ,age  :: Int
}

标签: haskell aeson
3条回答
We Are One
2楼-- · 2019-05-04 00:49

mniip's answer converts the JSON Object to a Map, which leads to a result list sorted by ID. If you don't need the results sorted in that fashion, it's probably better to use a more direct approach to speed things up. In particular, an Object is really just a HashMap Text Value, so we can use HashMap operations to work with it.

Note that I renamed the id field to ident, because most Haskell programmers will assume that id refers to the identity function in Prelude or to the more general identity arrow in Control.Category.

module Aes where
import Control.Applicative
import Data.Aeson
import Data.Text (Text)
import qualified Data.HashMap.Strict as HMS

data PersonInfo = PersonInfo { infoName :: Text, infoAge :: Int }

instance FromJSON PersonInfo where
-- Use mniip's definition here

data Person = Person { ident :: Text, name :: Text, age :: Int }

newtype PersonList = PersonList [Person]

instance FromJSON PersonList where
  parseJSON (Object v) = PersonList <$> HMS.foldrWithKey go (pure []) v
    where
      go i x r = (\(PersonInfo nm ag) rest -> Person i nm ag : rest) <$>
                     parseJSON x <*> r
  parseJSON _ = empty

Note that, like Alexander VoidEx Ruchkin's answer, this sequences the conversion from PersonInfo to Person explicitly within the Parser monad. It would therefore be easy to modify it to produce a parse error if the Person fails some sort of high-level validation. Alexander's answer also demonstrates the utility of the withObject combinator, which I'd have used if I'd known it existed.

查看更多
\"骚年 ilove
3楼-- · 2019-05-04 00:56

You cannot define an instance for [Person] literally, because aeson already includes an instance for [a], however you can create a newtype, and provide an instance for that.

Aeson also includes the instance FromJSON a => FromJSON (Map Text a), which means if aeson knows how to parse something, it knows how to parse a dict of that something.

You can define a temporary datatype resembling a value in the dict, then use the Map instance to define FromJSON PersonList, where newtype PersonList = PersonList [Person]:

data PersonInfo = PersonInfo { infoName :: Text, infoAge :: Int }

instance FromJSON PersonInfo where
    parseJSON (Object v) = PersonInfo <$> v .: "name" <*> v .: "age"
    parseJSON _ = mzero

data Person = Person { id :: Text, name :: Text, age :: Int }
newtype PersonList = PersonList [Person]

instance FromJSON PersonList where
    parseJSON v = fmap (PersonList . map (\(id, PersonInfo name age) -> Person id name age) . M.toList) $ parseJSON v
查看更多
戒情不戒烟
4楼-- · 2019-05-04 01:08

If you enable FlexibleInstances, you can make instance for [Person]. You can parse your object to Map Text Value and then parse each element in map:

{-# LANGUAGE UnicodeSyntax, OverloadedStrings, FlexibleInstances #-}

module Person (
    ) where

import Data.Aeson
import Data.Aeson.Types
import Data.Text.Lazy
import Data.Text.Lazy.Encoding
import Data.Map (Map)
import qualified Data.Map as M

data Person = Person {
    id ∷ Text,
    name ∷ Text,
    age ∷ Int }
        deriving (Eq, Ord, Read, Show)

instance FromJSON [Person] where
    parseJSON v = do
        objs ← parseJSON v ∷ Parser (Map Text Value)
        sequence [withObject "person"
            (\v' → Person i <$> v' .: "name" <*> v' .: "age") obj | 
            (i, obj) ← M.toList objs]

test ∷ Text
test = "{\"bob_id\":{\"name\":\"bob\",\"age\":20},\"jack_id\":{\"name\":\"jack\",\"age\":25}}"

res ∷ Maybe [Person]
res = decode (encodeUtf8 test)
查看更多
登录 后发表回答