3
votes

I have a couple types, User and Post. A Post is created by a User.

My database looks the same as my types, which are

data User = { userID :: Integer, name :: String }
data Post = { content :: String, authorID :: Integer } -- authorID is the userID

Assuming I have valid To/FromJSON instances based on those types, how would I go about conditionally including a value in the output of my JSON?

As an example, I may want to look at a users "profile", which would include all Posts made by the User. Other times, I may want to search through the various Users in the system, so there is no point in returning the Posts that they have made.

I have no problem retrieving the Posts for a given User, nor do I have an issue retrieving the User of a given Post. I just want to know the best way to conditionally add a key to my JSON.

Someone mentioned to me that I should essentially add a field to User for posts :: Maybe [Post] and add an author :: Maybe User to my Post type. While this would work for rather small types, I feel that it would make certain types with a lot of relations quite large and potentially hard to maintain.

I've also thought of using a Map Text Data.Aeson.Value to build / serialize my information, but you lose most (all?) type safety by doing something like that.

EDIT

Based on the data types above, I have defined the following ToJSON instances.

instance ToJSON User where
  toJSON (User i n) = object [ "id" .= i, "name" .= n ]

instance ToJSON Post where
  toJSON (Post c a) = object [ "content" .= c, "author_id" .= a ]

This would give me "simple" json output, something like User:

{
  "id": 4,
  "name": "User Name"
}

Post:

{
  "content": "This is the content of my post",
  "author_id": 4
}

These can be useful, especially when searching for items. This allows to only show a limited amount of information.

I could do something along the lines of

instance ToJSON User where
  toJSON (User i n) = object [ "id" .= i, "name" .= n, "posts" .= posts i ]

Which would give me the Users information, as well as all posts associated with that user, assuming a valid definition of posts. This would give me JSON similar to

{
  "id": 4,
  "name": "User Name",
  "posts": [
    {
      "content": "This is some content",
      "author_id": 4
    }
  ]
}

I do not understand how I would be able to dynamically set which fields I would like to send down to the client. Is it possible to define multiple instances of ToJSON and choose which instance I would like to use for a given request? Or should I just do what was suggested before and include a posts field in my User type that can hold a Maybe [Post]?

1
I'm not sure I understand the question yet. What makes it hard to conditionally include a value? Can you give a short-ish example where your JSON includes "too much" and ideally also why you think it's hard to include less?Daniel Wagner
@DanielWagner I made an edit, I hope it clears things up a little.Justin Wood
Can't you just... sidestep the typeclass? I mean, you can write as many functions as you want of type Foo -> Value, they don't have to be named toJSON.Daniel Wagner

1 Answers

4
votes

I don't think there's a general answer to whether you should use dynamic JSON or stick to fixed ToJSON instances, but you can easily mix the two and add extra fields to JSON values that are generated from your instances.

From what you've written I guess you know Aeson represents an object as a Map Text Value? For arrays it uses Vector Value.

So you can call toJSON on your User and on the list of Posts and just insert the post array as an additional entry in the map if you want it:

{-# LANGUAGE OverloadedStrings #-}

import qualified Data.ByteString.Lazy.Char8 as B
import Data.Aeson
import Data.Aeson.Types
import qualified Data.HashMap.Strict as M

data User = User { userID :: Integer, name :: String }

data Post = Post { content :: String, authorID :: Integer }

instance ToJSON User where
    toJSON (User i n) = object [ "id" .= i, "name" .= n ]

instance ToJSON Post where
    toJSON (Post c a) = object [ "content" .= c, "author_id" .= a ]

babs = User { userID = 5, name = "babs" }

posts = [ Post { content = "blah", authorID = 5 } ]

userWithPosts = Object (M.insert "posts" v o)
  where
    Object o = toJSON babs
    v        = toJSON posts

main = B.putStrLn (encode userWithPosts)

The encode call gives you the combined JSON:

*Main B> main
{"name":"babs","id":5,"posts":[{"author_id":5,"content":"blah"}]}