PureScript Typeclass‑Based JSON Serialization with purescript‑psa
A concise guide to deriving FromJSON and ToJSON instances in PureScript using the purescript‑psa library, complete with a worked example, compile‑time safety, and common pitfalls.
19 Jan 2026, 11:50 UTC

What you’ll get
By the end of this article you’ll know how to declare a PureScript data type, automatically derive FromJSON and ToJSON instances with purescript‑psa, and verify that the round‑trip encoding/decoding works at compile time. You’ll also understand the key limitations and common mistakes that can trip up even seasoned PureScript developers.
Why use typeclass‑based JSON?
PureScript’s typeclass system lets you encode the shape of a value into the compiler. With FromJSON and ToJSON you get:
- Compile‑time guarantees that a value can be safely parsed or encoded.
- Zero runtime overhead beyond the generated code.
- Automatic handling of records, tuples, and sum types.
The purescript‑psa library implements these typeclasses by generating generic encoders/decoders that walk the record or sum type representation.
Step 1 – Set up the project
# Create a new PureScript project
purs init --name json-demo
cd json-demo
# Add the purescript-psa dependency
purs add purescript-psa
Open bower.json and ensure purescript-psa appears under dependencies. Run bower install if you’re using the Bower‑based workflow.
Step 2 – Define a data type and derive instances
Create src/Person.purs:
module Person where
import Prelude
import Data.Show (class Show)
import Data.Eq (class Eq)
import Data.Generic.Rep (class Generic)
import Data.Generic.Rep.Show (genericShow)
import Data.Generic.Rep.Eq (genericEq)
import Data.Generic.Rep.Generic (generic)
import PSA.Json (class FromJSON, class ToJSON, decode, encode)
-- | A simple record type
newtype Person = Person
{ name :: String
, age :: Int
}
derive instance genericPerson :: Generic Person _
derive instance eqPerson :: Eq Person
derive instance showPerson :: Show Person
-- | Derive JSON instances. Must be in the same module.
derive instance fromJSONPerson :: FromJSON Person
derive instance toJSONPerson :: ToJSON Person
Key points:
- The
newtypewrapper keeps the record distinct for type safety. - All three instances –
Generic,FromJSON,ToJSON– are declared in the same module. If they live elsewhere the compiler cannot resolve them. - Deriving automatically works for simple records and tuples. Complex nested types may need manual instance definitions.
Step 3 – Test the round‑trip
Create src/Test.purs:
module Test where
import Prelude
import Person (Person(..))
import PSA.Json (decode, encode)
import Data.Maybe (Maybe(..))
import Data.Eq ((==))
import Effect (Effect)
import Effect.Console (logShow)
-- | Encode a Person to JSON string, then decode it back.
roundTrip :: Person -> Effect Unit
roundTrip person = do
let json = encode person
logShow json
case decode json of
Just decoded -> logShow $ "Round‑trip works: " <> show (decoded == person)
Nothing -> logShow "Decoding failed"
main :: Effect Unit
main = roundTrip (Person { name: "Ada", age: 30 })
Compile and run:
purs compile src/**/*.purs
node .stack-work/dist/**/*.js
You should see the JSON string and a confirmation that the decoded value equals the original. If the compiler complains about missing instances, double‑check that the derive instance lines are in the same module as the Person definition.
Common pitfalls and how to avoid them
- Instance location: Instances must be in the same module as the type or re‑exported. Otherwise the compiler cannot find them.
- Manual vs. automatic derivation: Automatic
derive instanceworks only for types that match the generic representation. If you have a field that is an alias or a custom type without aGenericinstance, you’ll need to write a manualFromJSON/ToJSONimplementation. - Unsupported JSON shapes:
purescript‑psaexpects a known schema. Mixed‑type arrays or deeply nested untyped structures are not supported out of the box. - Raw string concatenation: Building JSON by hand bypasses the type system and can lead to runtime errors that the compiler would otherwise catch.
- Large schemas: While runtime performance is fine, compile times can increase noticeably for very large record trees. Consider splitting into smaller modules if you hit this.
Limitations to keep in mind
- The library does not support polymorphic fields that change type at runtime.
- Optional fields are represented as
Maybein the record; missing fields will decode toNothing. - When using sum types, the discriminator key defaults to
"_tag". If you need a custom key you must write a manual instance.
How to verify your setup
After compiling, run the test script. The console should output something like:
{ "name": "Ada", "age": 30 }
Round‑trip works: true
If you see a Nothing or a type error, check that:
- All
derive instancelines are present. - The module imports
PSA.Jsoncorrectly. - The JSON string matches the expected shape (e.g., field names are correct).
Conclusion
PureScript’s typeclass‑based JSON serialization via purescript‑psa gives you strong compile‑time safety while keeping runtime code minimal. By following the module‑level instance rule, leveraging automatic derivation for simple types, and being aware of the library’s shape constraints, you can reliably encode and decode JSON in your PureScript projects.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.