0

I'm trying to parse embedded JSON of the form

{
  "foo":"bar",
  "baz":"\{\"somekey\":\"someval\"\}"
}

with Aeson in Haskell. Here are my types:

data BaseType = BaseType { foo :: String, baz :: EmbeddedType } deriving(Show)

instance FromJSON BaseType where
  parseJSON = withObject "BaseType" $ \o -> do
    foo <- o .: "foo"
    baz <- o .: "baz"
    return $ BaseType { foo=foo, baz=baz }

data EmbeddedType = EmbeddedType { somekey :: String }

instance FromJSON EmbeddedType where
  parseJSON = withObject "EmbeddedType" $ \o -> do
    somekey <- o .: "somekey"
    return $ EmbeddedType {somekey=somekey}

Obviously, the FromJSON instance for BaseType doesn't work, since it sees it as a Value String instead of as more JSON for it to parse. I tried to find a way to use decodeEither in my FromJSON BaseType instance, but that required that I do some black magic to convert from String to ByteString, and I feel like there must be a neater way, possibly related to withEmbeddedJSON.

How can I make this work correctly?

thesecretmaster
  • 1,950
  • 1
  • 27
  • 39

1 Answers1

2

I guess it would be something like this (untested):

instance FromJSON BaseType where
  parseJSON = withObject "BaseType" $ \o -> do
    foo <- o .: "foo"
    bazText <- o .: "baz"
    baz <- withEmbeddedJSON "EmbeddedType" parseJSON (String bazText)
    return $ BaseType { foo=foo, baz=baz }

Or you could consider moving the call to withEmbeddedJSON into the EmbeddedType instance; then o .: "baz" should Just Work in the BaseType instance, at the cost of no longer having a handle onto a parser that just does EmbeddedType parsing without de-stringifying:

instance FromJSON BaseType where
  parseJSON = withObject "BaseType" $ \o -> do
    foo <- o .: "foo"
    baz <- o .: "baz"
    return $ BaseType { foo=foo, baz=baz }

instance FromJSON EmbeddedType where
  parseJSON = withEmbeddedJSON "EmbeddedType" . withObject "EmbeddedType" $ \o -> do
    somekey <- o .: "somekey"
    return $ EmbeddedType {somekey=somekey}
Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • 1
    you could do `baz_ <- o .: "baz"` and give `baz_` type `Text` (aeson does decoding anyway). That would save the `case HM.lookup ....`. – Li-yao Xia Dec 19 '18 at 16:36
  • I think you'll have to make an explicit call to `decode` in any case. (`parseJSON` still requires you to get a `Value` from somewhere) – Li-yao Xia Dec 19 '18 at 16:39
  • 1
    @Li-yaoXia `withEmbeddedJSON` calls `decode` for you (well, `eitherDecode`, anyway). – Daniel Wagner Dec 19 '18 at 16:41
  • Thank you for your help, and the code works, but would you mind helping me out a bit by explaining a couple things? I'm kinda new to Haskell, and I don't understand 1) how you're composing `withEmbeddedJSON` with `withObject`, 2) Your use of the `String` type constructor, and 3) How passing `parseJSON` into `withEmbeddedJSON` does what we want it to. – thesecretmaster Dec 19 '18 at 16:45
  • @thesecretmaster For (1) you might like [this question](https://stackoverflow.com/q/53725329/791604); for (2) I looked at the source of `withEmbeddedJSON` to see what it expected as input; for (3) type inference selects `parseJSON :: Value -> Parser EmbeddedType`, so it dispatches to your other instance for parsing the embedded JSON. – Daniel Wagner Dec 19 '18 at 16:49
  • @DanielWagner Thank you very much! – thesecretmaster Dec 19 '18 at 16:54