Parsing JSON with ezon for a composite data type

I have the following data type:

data DocumentOrDirectory = Document DocumentName DocumentContent 
                         | Directory DirectoryName [DocumentOrDirectory]

      

I came up with the following code for JSON. It works but needs improvement. It should convert document and directory separately, but I don't know how.

instance JSON.ToJSON DocumentOrDirectory where
    toJSON (Document documentName documentContent) = JSON.object
        [ "document" JSON..= JSON.object 
            [ "name" JSON..= (T.pack $ id documentName)
            , "content" JSON..= (T.pack $ id documentContent)
            ]
        ]
    toJSON (Directory dirName dirContent) = JSON.object
        [ "directory" JSON..= JSON.object 
            [ "name" JSON..= (T.pack $ id dirName)
            , "content" JSON..= JSON.toJSON dirContent
            ]
        ]

      

I need to be able to parse a DocumentOrDirectory object from JSON. This is what I came up with (doesn't work):

instance JSON.FromJSON DocumentOrDirectory where
    parseJSON (Object v@(Document documentName documentContent)) = 
        DocumentOrDirectory <$> documentName .: "name"
                            <*> documentContent .: "content"
    parseJSON (Object v@(Directory dirName dirContent) = 
        DocumentOrDirectory <$> dirName .: "name"
                            <*> dirContent .: "content"
    parseJSON _ = mzero

      

How do I modify my existing code to be able to convert data from JSON to JSON?

+3


source to share


1 answer


We approach this problem step by step.

First, I'll assume, for example, that the names and contents are just String

:

type DirectoryName = String
type DocumentName = String
type DocumentContent = String

      

You mentioned that you want to serialize Document

and Directory

separately. You might also want to work with them separately. Let's make them separate types:

data Document = Document DocumentName DocumentContent deriving Show
data Directory = Directory DirectoryName [DocumentOrDirectory] deriving Show
newtype DocumentOrDirectory = DocumentOrDirectory (Either Document Directory) deriving Show

      

It DocumentOrDirectory

is now an alias for the type or Either Document Directory

. We used newtype

it because we want to write our own instance for it. The default instance Either

won't work for us.

And let's define some helper functions:

liftDocument :: Document -> DocumentOrDirectory
liftDocument = DocumentOrDirectory . Left

liftDirectory :: Directory -> DocumentOrDirectory
liftDirectory = DocumentOrDirectory . Right

      

With these definitions, we can write individual instances ToJSON

:

instance ToJSON Document where
  toJSON (Document name content) = object [ "document" .= object [
    "name"    .= name,
    "content" .= content ]]

instance ToJSON Directory where
  toJSON (Directory name content) = object [ "directory" .= object [
    "name"    .= name,
    "content" .= content ]]

instance ToJSON DocumentOrDirectory where
  toJSON (DocumentOrDirectory (Left d))  = toJSON d
  toJSON (DocumentOrDirectory (Right d)) = toJSON d

      

We have to check how are serialized Document

and Directory

(I previously disabled JSON output):

*Main> let document = Document "docname" "lorem"
*Main> B.putStr (encode document)

{
  "document": {
    "content": "lorem",
    "name": "docname"
  }
}

*Main> let directory = Directory "dirname" [Left document, Left document]
*Main> B.putStr (encode directory) >> putChar '\n'

{
  "directory": {
    "content": [
      {
        "document": {
          "content": "lorem",
          "name": "docname"
        }
      },
      {
        "document": {
          "content": "lorem",
          "name": "docname"
        }
      }
    ],
    "name": "directory"
  }
}

      

B.putStr (encode $ liftDirectory directory)

will lead to the same!

The next step is to write decoders, FromJSON

instances. We can see that the key ( Directory

or Document

) indicates whether the underlying data is Directory

or Document

. So the JSON format is non-overlapping (unambiguous), so we can just try to parse Document

and then Directory

.

instance FromJSON Document where
  parseJSON (Object v) = maybe mzero parser $ HashMap.lookup "document" v
    where parser (Object v') = Document <$> v' .: "name"
                                        <*> v' .: "content"
          parser _           = mzero
  parseJSON _          = mzero

instance FromJSON Directory where
  parseJSON (Object v) = maybe mzero parser $ HashMap.lookup "directory" v
    where parser (Object v') = Directory <$> v' .: "name"
                                         <*> v' .: "content"
          parser _           = mzero
  parseJSON _          = mzero

instance FromJSON DocumentOrDirectory where
  parseJSON json = (liftDocument <$> parseJSON json) <|> (liftDirectory <$> parseJSON json)

      



And check:

*Main> decode $ encode directory :: Maybe DocumentOrDirectory
Just (DocumentOrDirectory (Right (Directory "directory" [DocumentOrDirectory (Left (Document "docname" "lorem")),DocumentOrDirectory (Left (Document "docname" "lorem"))])))

      


We could serialize data with a type tag inside the object data, then serialization and deserialization would look a little better:

instance ToJSON Document where
  toJSON (Document name content) = object [
    "type"    .= ("document" :: Text),
    "name"    .= name,
    "content" .= content ]

      

The generated document will look like this:

{
  "type": "document",
  "name": "docname",
  "content": "lorem"
}

      

And decoding:

instance FromJSON Document where
  -- We could have guard here
  parseJSON (Object v) = Document <$> v .: "name"
                                  <*> v .= "content" 

instance FromJSON DocumentOrDirectory where
  -- Here we check the type, and dynamically select appropriate subparser
  parseJSON (Object v) = do typ <- v .= "type"
                            case typ of
                              "document"  -> liftDocument $ parseJSON v
                              "directory" -> liftDirectory $ parseJSON v
                              _           -> mzero

      


In subtyped languages ​​like scala, you could:

sealed trait DocumentOrDirectory
case class Document(name: String, content: String) extends DocumentOrDirectory
case class Directory(name: String, content: Seq[DocumentOrDirectory]) extends DocumentOrDirectory

      

one could argue that this approach (which relies on subtyping) is more convenient. In Haskell we are more explicit: liftDocument

and liftDirectory

can be thought of as explicit types of coercions / upcasts if you like thinking about objects.


EDIT: Working code as entity

+4


source







All Articles