Skip to content

Commit b897f8e

Browse files
committed
Support null values when parsing optional fields
We want to be more lenient in what we accept. Up until now, we've required that the JSON we parse come in a specific format: optional values don't exist. Unfortunately, things aren't so consistent in the real world. Instead of failing if we see an optional field that has a `null` value, we parse it as though the value coming in was `Data.Maybe.Nothing`. This is akin to how record-based values work (like `Option.set'`). Since this changes the semantics of parsing, this is a breaking change, and we should deal with it as such. With this change, we ought to be able to be used in more places without causing undue burden.
1 parent ee3090d commit b897f8e

8 files changed

+106
-6
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,15 @@ We can give that a spin with some different JSON values:
330330
331331
> parse """{"name": "Pat", "title": "Dr."}"""
332332
(Right (Option.fromRecord { name: "Pat", title: "Dr." }))
333+
334+
> parse """{ "name": null }"""
335+
Right (Option.fromRecord {})
336+
337+
> parse """{ "title": null }"""
338+
Right (Option.fromRecord {})
339+
340+
> parse """{ "name": null, "title": null }"""
341+
Right (Option.fromRecord {})
333342
```
334343

335344
We can also produce some different JSON values:
@@ -531,6 +540,15 @@ We can give that a spin with some different JSON values:
531540
532541
> parse """{"name": "Pat", "title": "Dr."}"""
533542
(Right (Option.fromRecord { name: "Pat", title: "Dr." }))
543+
544+
> parse """{ "name": null }"""
545+
Right (Option.fromRecord {})
546+
547+
> parse """{ "title": null }"""
548+
Right (Option.fromRecord {})
549+
550+
> parse """{ "name": null, "title": null }"""
551+
Right (Option.fromRecord {})
534552
```
535553

536554
We can also produce some different JSON values:
@@ -828,6 +846,15 @@ We can give that a spin with some different JSON values:
828846
829847
> readJSON """{"name": "Pat", "title": "Dr."}"""
830848
(Right (Option.fromRecord { name: "Pat", title: "Dr." }))
849+
850+
> readJSON """{ "name": null }"""
851+
Right (Option.fromRecord {})
852+
853+
> readJSON """{ "title": null }"""
854+
Right (Option.fromRecord {})
855+
856+
> readJSON """{ "name": null, "title": null }"""
857+
Right (Option.fromRecord {})
831858
```
832859

833860
We can also produce some different JSON values:
@@ -947,6 +974,9 @@ We can give that a spin with some different JSON values:
947974
948975
> parse """{"name": "Pat", "title": "Dr."}"""
949976
(Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))
977+
978+
> parse """{ "name": "Pat", "title": null }"""
979+
Right (Option.recordFromRecord { name: "Pat" })
950980
```
951981

952982
We can also produce some different JSON values:
@@ -1162,6 +1192,9 @@ We can give that a spin with some different JSON values:
11621192
11631193
> parse """{"name": "Pat", "title": "Dr."}"""
11641194
(Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))
1195+
1196+
> parse """{ "name": "Pat", "title": null }"""
1197+
Right (Option.recordFromRecord { name: "Pat" })
11651198
```
11661199

11671200
Notice that we have to supply a `"name"` field in the JSON input otherwise it will not parse.
@@ -1474,6 +1507,9 @@ We can give that a spin with some different JSON values:
14741507
14751508
> parse """{"name": "Pat", "title": "Dr."}"""
14761509
(Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))
1510+
1511+
> parse """{ "name": "Pat", "title": null }"""
1512+
Right (Option.recordFromRecord { name: "Pat" })
14771513
```
14781514

14791515
We can also produce some different JSON values:

src/Option.purs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ import Data.Argonaut.Decode.Error as Data.Argonaut.Decode.Error
133133
import Data.Argonaut.Encode.Class as Data.Argonaut.Encode.Class
134134
import Data.Codec as Data.Codec
135135
import Data.Codec.Argonaut as Data.Codec.Argonaut
136+
import Data.Codec.Argonaut.Compat as Data.Codec.Argonaut.Compat
136137
import Data.Either as Data.Either
137138
import Data.List as Data.List
138139
import Data.Maybe as Data.Maybe
@@ -605,9 +606,11 @@ else instance decodeJsonOptionCons ::
605606
Data.Either.Either Data.Argonaut.Decode.Error.JsonDecodeError (Option option)
606607
decodeJsonOption _ object' = case Foreign.Object.lookup key object' of
607608
Data.Maybe.Just json -> do
608-
value <- Data.Argonaut.Decode.Class.decodeJson json
609+
value' <- Data.Argonaut.Decode.Class.decodeJson json
609610
option <- option'
610-
Data.Either.Right (insert label value option)
611+
case value' of
612+
Data.Maybe.Just value -> Data.Either.Right (insert label value option)
613+
Data.Maybe.Nothing -> Data.Either.Right (insertField label option)
611614
Data.Maybe.Nothing -> do
612615
option <- option'
613616
Data.Either.Right (insertField label option)
@@ -1509,9 +1512,11 @@ else instance jsonCodecOptionCons ::
15091512
decode object' = do
15101513
option <- Data.Codec.Argonaut.decode option' object'
15111514
case Foreign.Object.lookup key object' of
1512-
Data.Maybe.Just json -> case Data.Codec.Argonaut.decode codec json of
1515+
Data.Maybe.Just json -> case Data.Codec.Argonaut.decode (Data.Codec.Argonaut.Compat.maybe codec) json of
15131516
Data.Either.Left error -> Data.Either.Left (Data.Codec.Argonaut.AtKey key error)
1514-
Data.Either.Right value -> Data.Either.Right (insert label value option)
1517+
Data.Either.Right value' -> case value' of
1518+
Data.Maybe.Just value -> Data.Either.Right (insert label value option)
1519+
Data.Maybe.Nothing -> Data.Either.Right (insertField label option)
15151520
Data.Maybe.Nothing -> Data.Either.Right (insertField label option)
15161521

15171522
encode ::
@@ -1833,9 +1838,11 @@ else instance readForeignOptionCons ::
18331838
true ->
18341839
Control.Monad.Except.except case Control.Monad.Except.runExcept (Foreign.Index.readProp key foreign') of
18351840
Data.Either.Left errors -> Data.Either.Left (map (Foreign.Index.errorAt key) errors)
1836-
Data.Either.Right value' -> case Control.Monad.Except.runExcept (Simple.JSON.readImpl value') of
1841+
Data.Either.Right value'' -> case Control.Monad.Except.runExcept (Simple.JSON.readImpl value'') of
18371842
Data.Either.Left errors -> Data.Either.Left (map (Foreign.Index.errorAt key) errors)
1838-
Data.Either.Right value -> Data.Either.Right (insert label value option)
1843+
Data.Either.Right value' -> case value' of
1844+
Data.Maybe.Just value -> Data.Either.Right (insert label value option)
1845+
Data.Maybe.Nothing -> Data.Either.Right (insertField label option)
18391846
false -> pure (insertField label option)
18401847
where
18411848
key :: String

test/HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptArgonaut.purs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,15 @@ spec_parse =
7979
Test.Spec.Assertions.shouldEqual
8080
(parse """{ "name": "Pat", "title": "Dr." }""")
8181
(Data.Either.Right (Option.fromRecord { name: "Pat", title: "Dr." }))
82+
Test.Spec.it "doesn't fail a null name" do
83+
Test.Spec.Assertions.shouldEqual
84+
(parse """{ "name": null }""")
85+
(Data.Either.Right (Option.fromRecord {}))
86+
Test.Spec.it "doesn't fail for a null title" do
87+
Test.Spec.Assertions.shouldEqual
88+
(parse """{ "title": null }""")
89+
(Data.Either.Right (Option.fromRecord {}))
90+
Test.Spec.it "doesn't fail for a null name or title" do
91+
Test.Spec.Assertions.shouldEqual
92+
(parse """{ "name": null, "title": null }""")
93+
(Data.Either.Right (Option.fromRecord {}))

test/HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptCodecArgonaut.purs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,15 @@ spec_parse =
8585
Test.Spec.Assertions.shouldEqual
8686
(parse """{ "name": "Pat", "title": "Dr." }""")
8787
(Data.Either.Right (Option.fromRecord { name: "Pat", title: "Dr." }))
88+
Test.Spec.it "doesn't fail a null name" do
89+
Test.Spec.Assertions.shouldEqual
90+
(parse """{ "name": null }""")
91+
(Data.Either.Right (Option.fromRecord {}))
92+
Test.Spec.it "doesn't fail for a null title" do
93+
Test.Spec.Assertions.shouldEqual
94+
(parse """{ "title": null }""")
95+
(Data.Either.Right (Option.fromRecord {}))
96+
Test.Spec.it "doesn't fail for a null name or title" do
97+
Test.Spec.Assertions.shouldEqual
98+
(parse """{ "name": null, "title": null }""")
99+
(Data.Either.Right (Option.fromRecord {}))

test/HowTo.DecodeAndEncodeJSONWithOptionalValuesInPureScriptSimpleJSON.purs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,27 @@ spec_readJSON =
5858
Test.Spec.Assertions.shouldEqual
5959
(readJSON json)
6060
(Data.Either.Right (Option.fromRecord { name: "Pat", title: "Dr." }))
61+
Test.Spec.it "doesn't fail a null name" do
62+
let
63+
json :: String
64+
json = """{ "name": null }"""
65+
Test.Spec.Assertions.shouldEqual
66+
(readJSON json)
67+
(Data.Either.Right (Option.fromRecord {}))
68+
Test.Spec.it "doesn't fail for a null title" do
69+
let
70+
json :: String
71+
json = """{ "title": null }"""
72+
Test.Spec.Assertions.shouldEqual
73+
(readJSON json)
74+
(Data.Either.Right (Option.fromRecord {}))
75+
Test.Spec.it "doesn't fail for a null name or title" do
76+
let
77+
json :: String
78+
json = """{ "name": null, "title": null }"""
79+
Test.Spec.Assertions.shouldEqual
80+
(readJSON json)
81+
(Data.Either.Right (Option.fromRecord {}))
6182

6283
spec_writeJSON :: Test.Spec.Spec Unit
6384
spec_writeJSON =

test/HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptArgonaut.purs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ spec_parse =
7171
Test.Spec.Assertions.shouldEqual
7272
(parse """{ "name": "Pat", "title": "Dr." }""")
7373
(Data.Either.Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))
74+
Test.Spec.it "doesn't fail for null fields" do
75+
Test.Spec.Assertions.shouldEqual
76+
(parse """{ "name": "Pat", "title": null }""")
77+
(Data.Either.Right (Option.recordFromRecord { name: "Pat" }))

test/HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptCodecArgonaut.purs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,7 @@ spec_parse =
7777
Test.Spec.Assertions.shouldEqual
7878
(parse """{ "name": "Pat", "title": "Dr." }""")
7979
(Data.Either.Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))
80+
Test.Spec.it "doesn't fail for null fields" do
81+
Test.Spec.Assertions.shouldEqual
82+
(parse """{ "name": "Pat", "title": null }""")
83+
(Data.Either.Right (Option.recordFromRecord { name: "Pat" }))

test/HowTo.DecodeAndEncodeJSONWithRequiredAndOptionalValuesInPureScriptSimpleJSON.purs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ spec_parse =
5555
Test.Spec.Assertions.shouldEqual
5656
(parse """{ "name": "Pat", "title": "Dr." }""")
5757
(Data.Either.Right (Option.recordFromRecord { name: "Pat", title: "Dr." }))
58+
Test.Spec.it "doesn't fail for null fields" do
59+
Test.Spec.Assertions.shouldEqual
60+
(parse """{ "name": "Pat", "title": null }""")
61+
(Data.Either.Right (Option.recordFromRecord { name: "Pat" }))
5862

5963
spec_writeJSON :: Test.Spec.Spec Unit
6064
spec_writeJSON =

0 commit comments

Comments
 (0)