haskell-jsonlisted
Install: claude install-skill ivelten/claude-haskell-skills
# JSON Serialization with Aeson
## Generic vs manual — decide upfront
| Situation | Approach |
|---|---|
| All constructors use record syntax | Generic with `Options` |
| Any constructor lacks record syntax | Manual instances |
| The JSON shape diverges significantly from the type structure | Manual instances |
| Simple product type with no sum | Generic with `defaultOptions` + `fieldLabelModifier` |
Mixing approaches in one type is a maintenance trap. Pick one and apply it consistently to `ToJSON` and `FromJSON`.
---
## 1. Sum types with record syntax — generic `TaggedObject`
When every constructor uses record syntax, `genericToJSON` with `TaggedObject` produces a flat object with a discriminator and inlined fields.
```haskell
import Data.Aeson (ToJSON(..), FromJSON(..), genericToJSON, genericParseJSON, defaultOptions)
import Data.Aeson.Types (Options(..), SumEncoding(..), camelTo2)
data Payload
= TextPayload { payloadText :: Text }
| ImagePayload { payloadUrl :: Text, payloadWidth :: Int, payloadHeight :: Int }
| FilePayload { payloadFileId :: FileId, payloadMimeType :: Text }
deriving stock (Show, Eq, Generic)
payloadOptions :: Options
payloadOptions = defaultOptions
{ sumEncoding = TaggedObject { tagFieldName = "type", contentsFieldName = "data" }
, constructorTagModifier = camelTo2 '_' -- "ImagePayload" → "image_payload"
, fieldLabelModifier = camelTo2 '_' . dropPrefix "payload"
}
instance ToJSON Payload where toJSON = gener