0
votes

I have this weird JSON to parse containing nested JSON ... a string. So instead of

{\"title\": \"Lord of the rings\", \"author\": {\"666\": \"Tolkien\"}\"}"

I have

{\"title\": \"Lord of the rings\", \"author\": \"{\\\"666\\\": \\\"Tolkien\\\"}\"}"

Here's my (failed) attempt to parse the nested string using decode, inside an instance of FromJSON :

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Data.Maybe
import GHC.Generics
import Data.Aeson
import qualified Data.Map as M

type Authors = M.Map Int String

data Book = Book
  {
    title :: String,
    author :: Authors
  }
  deriving (Show, Generic)

decodeAuthors x =  fromJust (decode x :: Maybe Authors)

instance FromJSON Book where
  parseJSON = withObject "Book" $ \v -> do
    t <- v .: "title"
    a <- decodeAuthors <?> v .: "author"
    return  $ Book t a

jsonTest = "{\"title\": \"Lord of the rings\", \"author\": \"{\\\"666\\\": \\\"Tolkien\\\"}\"}"

test = decode jsonTest :: Maybe Book

Is there a way to decode the whole JSON in a single pass ? Thanks !

1

1 Answers

3
votes

A couple problems here.

First, your use of <?> is nonsensical. I'm going to assume it's a typo, and what you actually meant was <$>.

Second, the type of decodeAuthors is ByteString -> Authors, which means its parameter is of type ByteString, which means that the expression v .: "author" must be of type Parser ByteString, which means that there must be an instance FromJSON ByteString, but such instance doesn't exists (for reasons that escape me at the moment).

What you actually want is for v .: "author" to return a Parser String (or perhaps Parser Text), and then have decodeAuthors accept a String and convert it to ByteString (using pack) before passing to decode:

import Data.ByteString.Lazy.Char8 (pack)

decodeAuthors :: String -> Authors
decodeAuthors x = fromJust (decode (pack x) :: Maybe Authors)

(also note: it's a good idea to give you declarations type signatures that you think they should have. This lets the compiler point out errors earlier)


Edit:

As @DanielWagner correctly points out, pack may garble Unicode text. If you want to handle it correctly, use Data.ByteString.Lazy.UTF8.fromString from utf8-string to do the conversion:

import Data.ByteString.Lazy.UTF8 (fromString)

decodeAuthors :: String -> Authors
decodeAuthors x = fromJust (decode (fromString x) :: Maybe Authors)

But in that case you should also be careful about the type of jsonTest: the way your code is written, its type would be ByteString, but any non-ASCII characters that may be inside would be cut off because of the way IsString works. To preserve them, you need to use the same fromString on it:

jsonTest = fromString "{\"title\": \"Lord of the rings\", \"author\": \"{\\\"666\\\": \\\"Tolkien\\\"}\"}"