Skip to content

Commit 05d25c2

Browse files
Update README.md
address feedback from @paldepind and improve relevant sections of the tutorial
1 parent c96395f commit 05d25c2

File tree

1 file changed

+65
-59
lines changed

1 file changed

+65
-59
lines changed

README.md

Lines changed: 65 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
[![Maintainer: garyb](https://img.shields.io/badge/maintainer-garyb-lightgrey.svg)](http://github.com/garyb)
77
[![Maintainer: thomashoneyman](https://img.shields.io/badge/maintainer-thomashoneyman-lightgrey.svg)](http://github.com/thomashoneyman)
88

9-
`argonaut-codecs` provides codecs based on the `EncodeJson` and `DecodeJson` type classes, along with instances for common data types and combinators for encoding and decoding `Json` values.
9+
Argonaut is a library for working with JSON in PureScript. `argonaut-codecs` provides codecs based on the `EncodeJson` and `DecodeJson` type classes, along with instances for common data types and combinators for encoding and decoding `Json` values.
1010

1111
## Installation
1212

@@ -20,28 +20,23 @@ bower install purescript-argonaut-codecs
2020

2121
## Documentation
2222

23-
Module documentation is [published on Pursuit](https://pursuit.purescript.org/packages/purescript-argonaut-codecs).
23+
Module documentation is [published on Pursuit](https://pursuit.purescript.org/packages/purescript-argonaut-codecs).
2424

2525
You may also be interested in other libraries in the Argonaut ecosystem:
2626

27-
- [purescript-argonaut-core](https://github.com/purescript-contrib/purescript-argonaut-core) -- defines the `Json` type, along with basic parsing, printing, and folding functions which operate on it
27+
- [purescript-argonaut-core](https://github.com/purescript-contrib/purescript-argonaut-core) -- defines the `Json` type, along with basic parsing, printing, and folding functions
2828
- [purescript-argonaut-traversals](https://github.com/purescript-contrib/purescript-argonaut-traversals) -- defines prisms, traversals, and zippers for the `Json` type.
29-
30-
- [purescript-argonaut-generic](https://github.com/purescript-contrib/purescript-argonaut-generic) -- supports generic encoding and decoding for any type with a `Generic` instance, which can be used to define instances of `EncodeJson` and `DecodeJson`.
31-
32-
Codecs based on type classes are not always the appropriate approach for encoding and decoding JSON in your application. See [purescript-codec-argonaut](https://github.com/garyb/purescript-codec-argonaut) for an alternative, more explicit approach, which includes a comparison to this library.
29+
- [purescript-argonaut-generic](https://github.com/purescript-contrib/purescript-argonaut-generic) -- supports generic encoding and decoding for any type with a `Generic` instance
30+
- [purescript-codec-argonaut](https://github.com/garyb/purescript-codec-argonaut) -- an alternative approach for codecs, which are based on profunctors instead of type classes
3331

3432
## Quick Start
3533

3634
Use `encodeJson` to encode PureScript data types as `Json` and `decodeJson` to decode `Json` into PureScript types, with helpful error messages if decoding fails.
3735

3836
```purs
39-
type User =
40-
{ name :: String
41-
, age :: Maybe Int
42-
}
37+
type User = { name :: String, age :: Maybe Int }
4338
44-
-- We get encoding and decoding for free because of the `EncodeJson` instances
39+
-- We get encoding and decoding for free because of the `EncodeJson` instances
4540
-- for records, strings, integers, and `Maybe`, along with many other common
4641
-- PureScript types.
4742
@@ -66,15 +61,9 @@ Right { name: "Tom", age: Just 25 }
6661
Left "JSON was missing expected field: age"
6762
```
6863

69-
This library provides helpful combinators for writing `EncodeJson` and `DecodeJson` instances for your own data types. See [the tutorial](#Tutorial) for a detailed walkthrough of this library.
70-
7164
## Tutorial
7265

73-
Argonaut is a library for working with JSON in PureScript.
74-
75-
The core `Json` data type and basic parsing, printing and folding functions are defined in the `argonaut-core` library, but these functions are low-level.
76-
77-
This library, `argonaut-codecs`, provides type classes and combinators for convenient (and often automatic) encoding and decoding of `Json` for data types in your application, and includes instances for encoding and decoding most common PureScript types.
66+
This library provides provides type classes and combinators for convenient encoding and decoding of `Json` for data types in your application, and includes instances for encoding and decoding most common PureScript types.
7867

7968
As a brief aside: this library works with `Json` values, not raw JSON strings.
8069

@@ -83,43 +72,58 @@ As a brief aside: this library works with `Json` values, not raw JSON strings.
8372

8473
#### Setup
8574

86-
We recommend following along with this tutorial in the repl. Start a repl and import these modules -- later, we'll define some types to encode and decode.
75+
You can follow along with this tutorial in a repl. You should install these dependencies:
76+
77+
```sh
78+
# with Spago
79+
spago install argonaut-codecs validation
80+
81+
# with Bower
82+
bower install purescript-argonaut-codecs purescript-validation
83+
```
84+
85+
Next, import the modules used in this tutorial -- you can also install `argonaut` and only import `Data.Argonaut` if you'd like to cut down on imports:
8786

8887
```
8988
import Prelude
9089
91-
import Data.Argonaut
90+
import Control.Alternative
91+
import Data.Argonaut.Core
92+
import Data.Argonaut.Encode
93+
import Data.Argonaut.Decode
94+
import Data.Argonaut.Parser
9295
import Data.Maybe
9396
import Data.Either
97+
import Data.Validation.Semigroup
9498
```
9599

96-
> Tip: you can place this snippet in a `.purs-repl` file so the imports are loaded automatically when you run `pulp repl` or `spago repl`.
100+
> Tip: you can place this snippet in a `.purs-repl` file so the imports are loaded automatically when you run `spago repl` or `pulp repl`.
97101
98-
### Automatic Encoding & Decoding
102+
### Automatic Encoding & Decoding
99103

100-
The `EncodeJson` and `DecodeJson` type classes let you rely on instances for common data types to automatically encode and decode `Json`.
101-
102-
Let's explored automatic encoding and decoding using a type typical of PureScript applications as our example:
104+
The `EncodeJson` and `DecodeJson` type classes let you rely on instances for common data types to automatically encode and decode `Json`. Let's explore automatic encoding and decoding using a type typical of PureScript applications as our example:
103105

104106
```purs
105107
type User =
106-
{ name :: String
108+
{ name :: String
107109
, age :: Maybe Int
108-
, team :: Maybe String
110+
, team :: Maybe String
109111
}
110112
```
111113

112114
> Tip: If you're following along in the repl, you can either define this type on one line or use `:paste` to input multiple lines followed by Ctrl+D to end the paste.
113115
114116
##### Automatic encoding with `EncodeJson` and `encodeJson`
115117

116-
Our `User` type is made up of several other types: `Record`, `Maybe`, `Int`, and `String`. Each of these types have instances for `EncodeJson`, which means that we can use the `encodeJson` function to transform them into `Json`:
118+
We can automatically encode `Json` using the `EncodeJson` type class ([pursuit](https://pursuit.purescript.org/packages/purescript-argonaut-codecs/docs/Data.Argonaut.Encode#t:EncodeJson)).
119+
120+
Our `User` type is made up of several other types: `Record`, `Maybe`, `Int`, and `String`. Each of these types have instances for `EncodeJson`, which means that we can use the `encodeJson` function with them. Integers and strings will be encoded directly to `Json`, while container types like `Record` and `Maybe` will require on all of the types they contain to also have `EncodeJson` instances.
117121

118122
```purs
119123
encodeJson :: EncodeJson a => a -> Json
120124
```
121125

122-
> Tip: There is no `Show` instance for `Json`. To print a `Json` value as a valid JSON string, use `stringify` -- it's the same as the [JavaScript `stringify` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
126+
> Tip: There is no `Show` instance for `Json`. To print a `Json` value as a valid JSON string, use `stringify` -- it's the same as the [JavaScript `stringify` method](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify).
123127
124128
```purs
125129
> user = { name: "Tom", age: Just 25, team: Just "Red Team" } :: User
@@ -129,7 +133,9 @@ encodeJson :: EncodeJson a => a -> Json
129133

130134
##### Automatic decoding with `DecodeJson` and `decodeJson`
131135

132-
Every type within `User` has an instance for `DecodeJson`, which means we can use the `decodeJson` function to try to decode a `Json` value into our type:
136+
We can automatically decode `Json` using the `DecodeJson` type class ([pursuit](https://pursuit.purescript.org/packages/purescript-argonaut-codecs/docs/Data.Argonaut.Decode#t:DecodeJson)).
137+
138+
Every type within `User` has an instance for `DecodeJson`, which means we can use the `decodeJson` function to try to decode a `Json` value into our type. Once again, integer and string values will be decoded directly from the `Json`, but containing types like `Record` and `Maybe` will also require instances for the types they contain.
133139

134140
```purs
135141
decodeJson :: DecodeJson a => Json -> Either String a
@@ -141,7 +147,7 @@ decodeJson :: DecodeJson a => Json -> Either String a
141147
> userJsonString = """{ "name": "Tom", "age": 25, "team": null }"""
142148
> decodedUser = decodeJson =<< jsonParser userJsonString
143149
144-
# there is no `Show` instance for `Json`, so we'll stringify
150+
# there is no `Show` instance for `Json`, so we'll stringify
145151
# the decoded result so it can be displayed in the repl
146152
> map stringify decodedUser
147153
Right "{\"name\":\"Tom\",\"age\":25,\"team\":null}"
@@ -170,7 +176,7 @@ Let's explore the combinators provided by `argonaut-codecs` for encoding and dec
170176
> Remember that you can write multi-line definitions using by typing :paste in the repl, and then using Ctrl+D to exit when you're done.
171177
172178
```purs
173-
newtype AppUser = AppUser
179+
newtype AppUser = AppUser
174180
{ name :: String
175181
, age :: Maybe Int
176182
, team :: Team
@@ -185,7 +191,7 @@ data Team
185191

186192
To encode JSON, you must decide on a way to represent your data using only primitive JSON types (strings, numbers, booleans, arrays, objects, or null). Since PureScript's string, number, boolean, and array types already have `EncodeJson` instances, your responsibility is to find a way to transform your data types to those more primitive types so they can be encoded.
187193

188-
Let's start with our `Team` type, which doesn't have an `EncodeJson` instance yet. It can be represented in JSON by simple strings, so let's write a function to convert `Team` to a `String`:
194+
Let's start with our `Team` type, which doesn't have an `EncodeJson` instance yet. It can be represented in JSON by simple strings, so let's write a function to convert `Team` to a `String`:
189195

190196
```purs
191197
teamToString :: Team -> String
@@ -207,11 +213,11 @@ instance encodeJsonTeam :: EncodeJson Team where
207213
encodeJson team = encodeJson (teamToString team)
208214
```
209215

210-
If your type can be converted easily to a `String`, `Number`, or `Boolean`, then its `EncodeJson` instance will most likely look like the one we've written for `Team`.
216+
If your type can be converted easily to a `String`, `Number`, or `Boolean`, then its `EncodeJson` instance will most likely look like the one we've written for `Team`.
211217

212218
Most reasonably complex data types are best represented as objects, however. We can use combinators from `Data.Argonaut.Encode.Combinators` to conveniently encode `Json` objects manually. You'll provide `String` keys and values which can be encoded to `Json`.
213219

214-
- Use `:=` (`assoc`) to encode a key/value pair where the key must exist; encoding the key `"team"` and value `Nothing ` will insert the key `"team"` with the value `null`.
220+
- Use `:=` (`assoc`) to encode a key/value pair where the key must exist; encoding the key `"team"` and value `Nothing` will insert the key `"team"` with the value `null`.
215221
- Use `~>` (`extend`) to provide more key/value pairs after using `:=`.
216222
- Use `:=?` (`assocOptional`) to encode a key/value pair where the key _may_ exist; encoding the key `"age"` and value `Nothing` will not insert the `"age"` key.
217223
- Use `~>?` (`extendOptional`) to provide more key/value pairs after using `:=?`.
@@ -220,7 +226,7 @@ Let's use these combinators to encode a `Json` object from our `AppUser` record.
220226

221227
```purs
222228
instance encodeJsonAppUser :: EncodeJson AppUser where
223-
encodeJson (AppUser { name, age, team }) =
229+
encodeJson (AppUser { name, age, team }) =
224230
"name" := name -- inserts "name": "Tom"
225231
~> "age" :=? age -- inserts "age": "25" (if Nothing, does not insert anything)
226232
~>? "team" := team -- inserts "team": "Red Team"
@@ -268,12 +274,10 @@ If your type can be represented easily with a `String`, `Number`, `Boolean`, or
268274

269275
However, quite often your data type will require representation as an object. This library provides combinators in `Data.Argonaut.Decode.Combinators` which are useful for decoding objects into PureScript types by looking up keys in the object and decoding them according to their `DecodeJson` instances.
270276

271-
- Use `.:` (`getField`) to decode a field where the value must exist
272-
- Use `.:?` (`getFieldOptional'`) to decode a field where the key may not exist, or the value may be `null`
273-
- Use `.:!` (`getFieldOptional`) to decode a field where the key may not exist. If the key exists, the value will be decoded -- even if it is `null`. This is rare; most of the time you want `.:?` instead.
274-
- Use `.!=` (`defaultField`) to provide a default value for a field which may not exist. If decoding fails, you'll still get an error; if decoding succeeds with a value of type `Maybe a`, then this default value will handle the `Nothing` case.
277+
- Use `.:` (`getField`) to decode a field; if the field is missing, this will decode to a `Maybe`
278+
- Use `.=` (`defaultField`) to provide a default value for a field which may not exist. If decoding fails, you'll still get an error; if decoding succeeds with a value of type `Maybe a`, then this default value will handle the `Nothing` case.
275279

276-
Let's use these combinators to decode a `Json` object into our `AppUser` record.
280+
Let's use these combinators to decode a `Json` object into our `AppUser` record.
277281

278282
The `decodeJson` function returns an `Either String a` value; `Either` is a monad, which means we can use convenient `do` syntax to write our decoder. If a step in decoding succeeds, then its result is passed to the next step. If any step in decoding fails, the entire computation will abort with the error it encountered.
279283

@@ -282,8 +286,8 @@ instance decodeJsonAppUser :: DecodeJson AppUser where
282286
decodeJson json = do
283287
obj <- decodeJson json -- decode `Json` to `Object Json`
284288
name <- obj .: "name" -- decode the "name" key to a `String`
285-
age <- obj .:? "age" -- decode the "age" key to a `Maybe Int`
286-
team <- obj .:? "team" .!= RedTeam -- decode "team" to `Team`, defaulting to `RedTeam`
289+
age <- obj .: "age" -- decode the "age" key to a `Maybe Int`
290+
team <- obj .: "team" .!= RedTeam -- decode "team" to `Team`, defaulting to `RedTeam`
287291
-- if the field is missing or `null`
288292
pure $ AppUser { name, age, team }
289293
```
@@ -292,9 +296,11 @@ To recap: manually decoding your data type involves a few steps:
292296

293297
1. Ensure that all types you are decoding have a `DecodeJson` instance
294298

295-
2. Use `.:` to decode object fields which must exist, `.:?` for fields which may exist or may be null, and `.!=` to provide a default value for fields which may exist and are being decoded into a type which must exist.
299+
2. Use `.:` to decode object fields
300+
301+
3. Use `.!=` to provide a default value for fields which may exist in the `Json`, but must exist in the type you're decoding to (it's like `fromMaybe` for your ) d and are being decoded into a type which must exist.
296302

297-
3. It's common to use the `Either` monad for convenience when writing decoders. Any failed decoding step will abort the entire computation with that error. See [Solving Common Problems](#solving-common-problems) for alternative approaches to decoding.
303+
4. It's common to use the `Either` monad for convenience when writing decoders. Any failed decoding step will abort the entire computation with that error. See [Solving Common Problems](#solving-common-problems) for alternative approaches to decoding.
298304

299305
#### Deriving Instances
300306

@@ -370,9 +376,9 @@ instance decodeJsonUser :: DecodeJson User where
370376
uuid <- obj .: "uuid" <|> obj .: "uid" <|> ((_ .: "value") =<< obj .: "id")
371377
```
372378

373-
##### 2. Write multiple `encodeJson ` or `decodeJson` functions
379+
##### 2. Write multiple `encodeJson` or `decodeJson` functions
374380

375-
Another option is to have a default representation for the type implemented as the type class instance, but alternative `decodeJson` and `encodeJson` functions which can be used directly. For example, consider the case in which our `User` data can be sent to multiple sources. One source requires the data to be formatted as an object, and another requires it to be formatted as a two-element array.
381+
Another option is to have a default representation for the type implemented as the type class instance, but alternative `decodeJson` and `encodeJson` functions which can be used directly. For example, consider the case in which our `User` data can be sent to multiple sources. One source requires the data to be formatted as an object, and another requires it to be formatted as a two-element array.
376382

377383
In this case, our type class instance can use the default object encoding, and we can supply a separate `encodeJsonAsArray` function for use when required.
378384

@@ -389,13 +395,13 @@ encodeUserAsArray user = encodeJson [ user.uuid, user.name ]
389395
You may occasionally be unable to write `EncodeJson` or `DecodeJson` instances for a data type because it requires more information than just `Json` as its argument. For instance, consider this pair of types:
390396

391397
```purs
392-
data Author
398+
data Author
393399
= Following String -- you are subscribed to this author
394400
| NotFollowing String -- you aren't subscribed to this author
395401
| You -- you are the author
396402
397403
type BlogPost =
398-
{ title :: String
404+
{ title :: String
399405
, author :: Author
400406
}
401407
```
@@ -417,7 +423,7 @@ decodeJsonAuthor maybeUsername json = do
417423
Just (Username username) | author == username -> You
418424
-- user is not the author, or no one is logged in, so use the `following` flag
419425
otherwise -> author # if following then Following else NotFollowing
420-
426+
421427
decodeJsonBlogPost :: Maybe Username -> Json -> Either String BlogPost
422428
decodeJsonBlogPost username json = do
423429
obj <- decodeJson json
@@ -444,10 +450,10 @@ instance decodeJsonPreciseDateTime :: DecodeJson PreciseDateTime where
444450
decodeJson json = fromString =<< decodeJson json
445451
where
446452
fromString :: String -> Either String PreciseDateTime
447-
fromString =
453+
fromString =
448454
map PreciseDateTime
449-
<<< note "Could not parse RFC3339 string"
450-
<<< PDT.fromRFC3339String
455+
<<< note "Could not parse RFC3339 string"
456+
<<< PDT.fromRFC3339String
451457
<<< RFC3339String
452458
```
453459

@@ -461,15 +467,15 @@ For example, let's say we have a `User` type which occasionally gets bad input,
461467

462468
```purs
463469
newtype User = User
464-
{ name :: String
470+
{ name :: String
465471
, age :: Maybe Int
466472
, location :: String
467473
}
468474
469475
derive instance newtypeUser :: Newtype User _
470476
derive newtype instance showUser :: Show User
471477
472-
decodeUser :: Json -> Either (Array String) User
478+
decodeUser :: Json -> Either (Array String) User
473479
decodeUser json = do
474480
obj <- decodeJson json
475481
name <- obj .: "name"
@@ -494,7 +500,7 @@ decodeJsonV = either (invalid <<< pure) pure <<< decodeJson
494500
495501
-- a replacement for `getField`
496502
getFieldV :: forall a. DecodeJson a => Object Json -> String -> V (Array String) a
497-
getFieldV object key =
503+
getFieldV object key =
498504
either (invalid <<< pure) pure (object .: key)
499505
500506
-- a replacement for .:
@@ -504,7 +510,7 @@ infix 7 getFieldV as .:|
504510
With this new operator and applicative-do we can recreate our original decoder, except with accumulating errors this time:
505511

506512
```purs
507-
decodeUser :: Json -> Either (Array String) User
513+
decodeUser :: Json -> Either (Array String) User
508514
decodeUser json = do
509515
user <- toEither $ V.andThen (decodeJsonV json) \obj -> ado
510516
name <- obj .:| "name"
@@ -519,7 +525,7 @@ This decoder will now print all errors:
519525
```purs
520526
> import Data.Bifunctor (lmap)
521527
> decodeUser =<< lmap pure (jsonParser "{}")
522-
Left
528+
Left
523529
[ "Expected field \"name\""
524530
, "Expected field \"age\""
525531
, "Expected field \"location\""

0 commit comments

Comments
 (0)