├── .gitignore ├── LICENSE ├── README.md ├── TEACHING.md ├── elm-package.json ├── elm.json └── src └── Json └── Decode ├── Field.elm └── Flags.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules 3 | package.json 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Webbhuset AB 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Continuation-passing style JSON decoder in Elm 2 | 3 | This packages helps you writing JSON decoders in a [Continuation-passing](https://en.wikipedia.org/wiki/Continuation-passing_style) style. 4 | This enables you to use named bindings for field names which is very useful when 5 | decoding JSON objects to Elm records or custom types. 6 | 7 | 8 | * [Introduction](#introduction) 9 | * [Examples](#examples) 10 | * [Combine Fields](#combine-fields) 11 | * [Nested JSON Objects](#nested-json-objects) 12 | * [Fail decoder if values are invalid](#fail-decoder-if-values-are-invalid) 13 | * [Decode custom types](#decode-custom-types) 14 | * [How does this work?](#how-does-this-work) 15 | 16 | ## Introduction 17 | 18 | Let's say you have a `Person` record in Elm with the following requirements: 19 | 20 | ```elm 21 | type alias Person = 22 | { id : Int -- Field is mandatory, decoder should fail if field is missing in the JSON object 23 | , name : String -- Field is mandatory 24 | , maybeWeight : Maybe Int -- Field is optional in the JSON object 25 | , likes : Int -- Should default to 0 if JSON field is missing or null 26 | , hardcoded : String -- Should be hardcoded to "Hardcoded Value" for now 27 | } 28 | ``` 29 | The approach [suggested by the core JSON library](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#map3) is to use the `Json.Decode.mapN` family of decoders to build 30 | a record. 31 | 32 | ```elm 33 | import Json.Decode as Decode exposing (Decoder) 34 | 35 | person : Decoder Person 36 | person = 37 | Decode.map5 Person 38 | (Decode.field "id" Decode.int) 39 | (Decode.field "name" Decode.string) 40 | (Decode.maybe <| Decode.field "weight" Decode.int) 41 | (Decode.field "likes" Decode.int 42 | |> Decode.maybe 43 | |> Decode.map (Maybe.withDefault 0) 44 | ) 45 | (Decode.succeed "Hardcoded Value") 46 | ``` 47 | 48 | Using this package you can write the same decoder like this: 49 | 50 | ```elm 51 | import Json.Decode as Decode exposing (Decoder) 52 | import Json.Decode.Field as Field 53 | 54 | person : Decoder Person 55 | person = 56 | Field.require "name" Decode.string <| \name -> 57 | Field.require "id" Decode.int <| \id -> 58 | Field.optional "weight" Decode.int <| \maybeWeight -> 59 | Field.attempt "likes" Decode.int <| \maybeLikes -> 60 | 61 | Decode.succeed 62 | { name = name 63 | , id = id 64 | , maybeWeight = maybeWeight 65 | , likes = Maybe.withDefault 0 maybeLikes 66 | , hardcoded = "Hardcoded Value" 67 | } 68 | ``` 69 | 70 | The main advantages over using `mapN` are: 71 | 72 | * Record field order does not matter. Named bindings are used instead of field order. 73 | You can change the order of the fields in the type declaration (`type alias Person ...`) without breaking the decoder. 74 | * Easier to see how the record is connected to the JSON object - especially when there are many fields. 75 | Sometimes the JSON fields have different names than your Elm record. 76 | * Easier to add fields down the line. 77 | * With the `mapN` approach, if all fields of the record are of the same type and you mess up the field order, you 78 | won't get any compiler error. Things will appear OK but field values will be transposed. 79 | Since this package uses named bindings it is much easier to get things right. 80 | * Sometimes fields needs futher validation / processing. See below examples. 81 | * If you have more than 8 fields in your object you can't use the `Json.Decode.mapN` approach since 82 | [map8](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#map8) is the largest map function. 83 | 84 | ## Examples 85 | 86 | ### Combine fields 87 | 88 | In this example the JSON object contains both `firstname` and `lastname`, but the Elm record only has `name`. 89 | 90 | **JSON** 91 | ```json 92 | { 93 | "firstname": "John", 94 | "lastname": "Doe", 95 | "age": 42 96 | } 97 | ``` 98 | **Elm** 99 | ```elm 100 | 101 | type alias Person = 102 | { name : String 103 | , age : Int 104 | } 105 | 106 | person : Decoder Person 107 | person = 108 | Field.require "firstname" Decode.string <| \firstname -> 109 | Field.require "lastname" Decode.string <| \lastname -> 110 | Field.require "age" Decode.int <| \age -> 111 | 112 | Decode.succeed 113 | { name = firstname ++ " " ++ lastname 114 | , age = age 115 | } 116 | ``` 117 | 118 | ### Nested JSON objects 119 | 120 | Using `requireAt` or `attemptAt` lets you reach down into nested objects. This is a 121 | common use case when decoding graphQL responses. 122 | 123 | **JSON** 124 | ```json 125 | { 126 | "id": 321, 127 | "title": "About JSON decoders", 128 | "author": { 129 | "id": 123, 130 | "name": "John Doe", 131 | }, 132 | "content": "..." 133 | } 134 | ``` 135 | **Elm** 136 | ```elm 137 | 138 | type alias BlogPost = 139 | { title : String 140 | , author : String 141 | , content : String 142 | } 143 | 144 | blogpost : Decoder BlogPost 145 | blogpost = 146 | Field.require "title" Decode.string <| \title -> 147 | Field.requireAt ["author", "name"] Decode.string <| \authorName -> 148 | Field.require "content" Decode.string <| \content -> 149 | 150 | Decode.succeed 151 | { title = title 152 | , author = authorName 153 | , content = content 154 | } 155 | ``` 156 | 157 | ### Fail decoder if values are invalid 158 | 159 | Here the decoder should fail if the person is younger than 18 yers old. 160 | 161 | **JSON** 162 | ```json 163 | { 164 | "name": "John Doe", 165 | "age": 42 166 | } 167 | ``` 168 | **Elm** 169 | ```elm 170 | type alias Person = 171 | { name : String 172 | , age : Int 173 | } 174 | 175 | person : Decoder Person 176 | person = 177 | Field.require "name" Decode.string <| \name -> 178 | Field.require "age" Decode.int <| \age -> 179 | 180 | if age < 18 then 181 | Decode.fail "You must be an adult" 182 | else 183 | Decode.succeed 184 | { name = name 185 | , age = age 186 | } 187 | ``` 188 | 189 | ### Decode custom types 190 | 191 | You can also use this package to build decoders for custom types. 192 | 193 | **JSON** 194 | ```json 195 | { 196 | "name": "John Doe", 197 | "id": 42 198 | } 199 | ``` 200 | **Elm** 201 | ```elm 202 | type User 203 | = Anonymous 204 | | Registered Int String 205 | 206 | user : Decoder User 207 | user = 208 | Field.attempt "id" Decode.int <| \maybeID -> 209 | Field.attempt "name" Decode.string <| \maybeName -> 210 | 211 | case (maybeID, maybeName) of 212 | (Just id, Just name) -> 213 | Registered id name 214 | |> Decode.succeed 215 | _ -> 216 | Decode.succeed Anonymous 217 | ``` 218 | 219 | ## How does this work? 220 | 221 | The following documentation assumes you are familiar with the following functions: 222 | 223 | 1. [Json.Decode.field](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#field) 224 | 2. [Json.Decode.map](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#map) 225 | 3. [Json.Decode.andThen](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#andThen) 226 | 4. Function application operator ([<|](https://package.elm-lang.org/packages/elm/core/latest/Basics#(<|))) 227 | 228 | You can read more about those in [this guide](https://github.com/webbhuset/elm-json-decode/blob/master/TEACHING.md) by 229 | Richard Feldman. 230 | 231 | Consider this simple example: 232 | 233 | ```elm 234 | import Json.Decode as Decode exposing (Decoder) 235 | 236 | type alias User = 237 | { id : Int 238 | , name : String 239 | } 240 | 241 | 242 | user : Decoder User 243 | user = 244 | Decode.map2 User 245 | (Decode.field "id" Decode.int) 246 | (Decode.field "name" Decode.string) 247 | ``` 248 | 249 | Here, `map2` from [elm/json](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#map2) is used to decode a JSON object to a record. 250 | The record constructor function is used (`User : Int -> String -> User`) to build the record. 251 | This means that the order in which fields are written in the type declaration matters. For example, if you 252 | change the order of fields `id` and `name` in yor record, you must also change the order of the two 253 | `(Decode.field ...)` rows to match the order of the record. 254 | 255 | To use named bindings instead you can use `Json.Decode.andThen` write a decoder like this: 256 | 257 | ```elm 258 | user : Decoder User 259 | user = 260 | Decode.field "id" Decode.int 261 | |> Decode.andThen 262 | (\id -> 263 | Decode.field "name" Decode.string 264 | |> Decode.andThen 265 | (\name -> 266 | Decode.succeed 267 | { id = id 268 | , name = name 269 | } 270 | ) 271 | ) 272 | ``` 273 | Now this looks ridiculous, but one thing is interesting: The record is 274 | constructed using named variables (in the innermost function). 275 | 276 | The fields are decoded one at the time with each decoded value being bound in turn to a 277 | continuation function using `andThen`. The innermost function has access to 278 | all the named argument variables from the outer scopes. 279 | 280 | The above code can be improved by using the helper function `require`. Here is 281 | the same decoder expressed in a cleaner way: 282 | 283 | ```elm 284 | module Json.Decode.Field exposing (require) 285 | 286 | require : String -> Decoder a -> (a -> Decoder b) -> Decoder b 287 | require fieldName valueDecoder continuation = 288 | Decode.field fieldName valueDecoder 289 | |> Decode.andThen continuation 290 | 291 | -- In User.elm 292 | module User exposing (user) 293 | 294 | import Json.Decode.Field as Field 295 | 296 | user : Decoder User 297 | user = 298 | Field.require "id" Decode.int 299 | (\id -> 300 | Field.require "name" Decode.string 301 | (\name -> 302 | Decode.succeed 303 | { id = id 304 | , name = name 305 | } 306 | ) 307 | ) 308 | ``` 309 | Nice: we got rid of some `andThen` noise. 310 | 311 | Now let's format the code in a more readable way: 312 | 313 | ```elm 314 | user : Decoder User 315 | user = 316 | Field.require "id" Decode.int (\id -> 317 | Field.require "name" Decode.string (\name -> 318 | 319 | Decode.succeed 320 | { id = id 321 | , name = name 322 | } 323 | )) 324 | ``` 325 | 326 | We can also eliminate the parenthesis by using the backwards 327 | [function application operator](https://package.elm-lang.org/packages/elm/core/latest/Basics#(<|)) (`<|`). 328 | 329 | ```elm 330 | user : Decoder User 331 | user = 332 | Field.require "id" Decode.int <| \id -> 333 | Field.require "name" Decode.string <| \name -> 334 | 335 | Decode.succeed 336 | { id = id 337 | , name = name 338 | } 339 | ``` 340 | 341 | This reads quite nicely. It's like two paragraphs. 342 | 343 | * In the first paragraph we extract everything we need from the JSON object and 344 | bind each value to a variable. Keeping the field decoder and the variable on the same row makes it 345 | easy to read. 346 | * In the second paragraph we build the actual Elm type using all the collected values. 347 | 348 | It kind of maps to natural language: 349 | 350 | > `require` a `Field` called `"id"` and `Decode` an `int`, bind the result to `id`\ 351 | > `require` a `Field` called `"name"` and `Decode` a `string`, bind the result to `name` 352 | > 353 | > The `Decode` will `succeed` with `{id = id, name = name}` 354 | 355 | 356 | This way of formatting the code kind of resembles the `do` notation syntax found in Haskell or Pure Script. 357 | 358 | ```haskell 359 | user : Decoder User 360 | user = do 361 | id <- Field.require "id" Decode.int 362 | name <- Field.require "name" Decode.string 363 | 364 | return 365 | { id = id 366 | , name = name 367 | } 368 | ``` 369 | 370 | -------------------------------------------------------------------------------- /TEACHING.md: -------------------------------------------------------------------------------- 1 | To show how this package works, we'll go through these steps: 2 | 3 | 1. `field` 4 | 2. `map` 5 | 3. `andThen` 6 | 4. `require` 7 | 5. `<|` 8 | 9 | ## 1. `field` 10 | 11 | `Decode.field` decodes a particular field from a JSON object: 12 | 13 | ```elm 14 | usernameDecoder : Decoder String 15 | usernameDecoder = 16 | Decode.field "username" Decode.string 17 | ``` 18 | 19 | This decoder will decode the string `"rtfeldman"` from the following JSON: 20 | 21 | ```js 22 | {"id": 5, "username": "rtfeldman", "name": "Richard Feldman"} 23 | ``` 24 | 25 | However, this decoder would fail if any of the following were true: 26 | 27 | * It was not run on a JSON **object** 28 | * The object did not have a field called **`username`** 29 | * The `username` field was not a **string** 30 | 31 | ## 2. `map` 32 | 33 | `List.map` uses a function to transform each value inside a list: 34 | 35 | ```elm 36 | List.map String.toLower [ "A", "B", "C" ] 37 | --> [ "a", "b", "c" ] 38 | ``` 39 | 40 | `Decode.map` uses a function to transform a successfully decoded value: 41 | 42 | ```elm 43 | Decode.map String.toLower Decode.string 44 | ``` 45 | 46 | This code returns a `Decoder String` which decodes a string, and then lowercases 47 | it. If decoding failed (for example because it tried to run this decoder on a 48 | number instead of a string), then `String.toLower` would not get called. 49 | 50 | ## 3. `andThen` 51 | 52 | `andThen` works like `map` except the transformation function has the power to 53 | change successes into failures. 54 | 55 | `andThen` lets us perform validation in a `Decoder`. For example, here we'll 56 | take a string and then: 57 | 58 | 1. Check if it's empty. If it's an empty string, fail decoding. 59 | 2. If it's not empty, lowercase it. 60 | 61 | ```elm 62 | validateAndTransform : String -> Decoder String 63 | validateAndTransform str = 64 | if String.isEmpty str then 65 | Decode.fail "the string was empty" 66 | else 67 | Decode.succeed (String.toLower str) 68 | 69 | 70 | decoder : Decoder String 71 | decoder = 72 | Decode.andThen validateAndTransform Decode.string 73 | ``` 74 | 75 | ## 4. `require` 76 | 77 | `require` is a convenience function which does a `Decode.field` 78 | followed by a `Decode.andThen`. 79 | 80 | ```elm 81 | lowercaseUsernameDecoder : Decoder String 82 | lowercaseUsernameDecoder = 83 | require "username" Decode.string (\str -> 84 | if String.isEmpty str then 85 | Decode.fail "the string was empty" 86 | else 87 | Decode.succeed (String.toLower str) 88 | ) 89 | ``` 90 | 91 | We can run this decoder on the following JSON: 92 | 93 | ```js 94 | {"id": 5, "username": "RTFELDMAN", "name": "Richard Feldman"} 95 | ``` 96 | 97 | It will give back `"rtfeldman"` because it lowercases the successfully decoded 98 | `"RTFELDMAN"`. 99 | 100 | This decoder would fail if any of the following were true: 101 | 102 | * It was not run on a JSON **object** 103 | * The object did not have a field called **`username`** 104 | * The `username` field was not a **string** 105 | * The `username` field was present, and a string, but the string was **empty** 106 | 107 | ## 5. `<|` 108 | 109 | We can chain several `require` calls together to decode into a record. 110 | 111 | ```elm 112 | type alias User = 113 | { id : Int, username : String, name : String } 114 | 115 | 116 | userDecoder : Decoder User 117 | userDecoder = 118 | require "username" string (\username -> 119 | require "id" int (\id -> 120 | require "name" string (\name -> 121 | succeed { id = id, username = username, name = name } 122 | ) 123 | ) 124 | ) 125 | ``` 126 | 127 | If any of these `require` calls fails to decode, the whole decoder will fail, 128 | because `require` uses `andThen` under the hood - meaning its transformation 129 | function can return a failure outcome. 130 | 131 | If they all succeed, the innermost transformation function will be run. It 132 | already has `id`, `name,` and `username` in scope, so it can use them to 133 | `Decode.succeed` with a `User` record. 134 | 135 | We can make this read more like a schema if we use `<|`. The `<|` operator can 136 | take the place of parentheses. These two lines of code do exactly the same thing: 137 | 138 | ```elm 139 | String.toLower (getStr something) 140 | String.toLower <| getStr something 141 | ``` 142 | 143 | `<|` expects a function on the left, and calls that function passing the value 144 | on the right. We can use this to write the above decoder without so many parentheses: 145 | 146 | ```elm 147 | userDecoder : Decoder User 148 | userDecoder = 149 | require "username" string <| \username -> 150 | require "id" int <| \id -> 151 | require "name" string <| \name -> 152 | succeed { id = id, username = username, name = name } 153 | ``` 154 | 155 | This way, the sequence of `require` calls can be read like a schema for the JSON. 156 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.2", 3 | "summary": "Decode JSON objects using chained functions (continuation style).", 4 | "repository": "https://github.com/webbhuset/elm-json-decode.git", 5 | "license": "MIT", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Json.Decode.Field" 11 | ], 12 | "dependencies": { 13 | "elm-lang/core": "5.1.1 <= v < 6.0.0" 14 | }, 15 | "elm-version": "0.18.0 <= v < 0.19.0" 16 | } 17 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "webbhuset/elm-json-decode", 4 | "summary": "Decode JSON objects using chained functions (continuation style).", 5 | "license": "MIT", 6 | "version": "1.2.0", 7 | "exposed-modules": [ 8 | "Json.Decode.Field", 9 | "Json.Decode.Flags" 10 | ], 11 | "elm-version": "0.19.0 <= v < 0.20.0", 12 | "dependencies": { 13 | "elm/core": "1.0.0 <= v < 2.0.0", 14 | "elm/json": "1.0.0 <= v < 2.0.0" 15 | }, 16 | "test-dependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /src/Json/Decode/Field.elm: -------------------------------------------------------------------------------- 1 | module Json.Decode.Field exposing (require, requireAt, optional, optionalAt, attempt, attemptAt) 2 | 3 | {-| # Decode JSON objects 4 | 5 | Since JSON values are not known until runtime there is no way 6 | of checking them at compile time. This means that there is a 7 | possibility a decoding operation can not be successfully completed. 8 | 9 | In that case there are two possible solutions: 10 | 11 | 1. Fail the whole decoding operation. 12 | 2. Deal with the missing value situation, either by defaulting to some value or by 13 | using a `Maybe` value 14 | 15 | In this module these two options are represented by `require`, `optional`, and 16 | `attempt`. 17 | 18 | * `require` fails if either the field is missing or is the wrong type. 19 | * `optional` succeeds with a `Nothing` if field is missing, but fails if the 20 | field exists and is the wrong type. 21 | * `attempt` always succeeds with a `Maybe` value. 22 | 23 | @docs require, requireAt, optional, optionalAt, attempt, attemptAt 24 | 25 | -} 26 | 27 | import Json.Decode as Decode exposing (Decoder) 28 | 29 | 30 | {-| Decode required fields. 31 | 32 | Example: 33 | 34 | import Json.Decode as Decode exposing (Decoder) 35 | 36 | user : Decoder User 37 | user = 38 | require "id" Decode.int <| \id -> 39 | require "name" Decode.string <| \name -> 40 | 41 | Decode.succeed 42 | { id = id 43 | , name = name 44 | } 45 | 46 | In this example the decoder will fail if: 47 | 48 | * The JSON value is not an object. 49 | * Any of the fields `"id"` or `"name"` are missing. If the object contains other fields 50 | they are ignored and will not cause the decoder to fail. 51 | * The value of field `"id"` is not an `Int`. 52 | * The value of field `"name"` is not a `String`. 53 | 54 | -} 55 | require : String -> Decoder a -> (a -> Decoder b) -> Decoder b 56 | require fieldName valueDecoder continuation = 57 | Decode.field fieldName valueDecoder 58 | |> Decode.andThen continuation 59 | 60 | 61 | {-| Decode required nested fields. Works the same as `require` but on nested fieds. 62 | 63 | import Json.Decode as Decode exposing (Decoder) 64 | 65 | blogPost : Decoder BlogPost 66 | blogPost = 67 | require "id" Decode.int <| \id -> 68 | require "title" Decode.string <| \title -> 69 | requireAt ["author", "name"] Decode.string <| \authorName -> 70 | 71 | Decode.succeed 72 | { id = id 73 | , title = title 74 | , author = authorName 75 | } 76 | -} 77 | requireAt : List String -> Decoder a -> (a -> Decoder b) -> Decoder b 78 | requireAt path valueDecoder continuation = 79 | Decode.at path valueDecoder 80 | |> Decode.andThen continuation 81 | 82 | 83 | {-| Decode optional fields. 84 | 85 | If the decode succeeds you get a `Just value`. If the field is missing you get 86 | a `Nothing`. 87 | 88 | Example: 89 | 90 | import Json.Decode as Decode exposing (Decoder) 91 | 92 | name : Decoder Name 93 | name = 94 | require "first" Decode.string <| \first -> 95 | optional "middle" Decode.string <| \maybeMiddle -> 96 | require "last" Decode.string <| \last -> 97 | 98 | Decode.succeed 99 | { first = first 100 | , middle = Maybe.withDefault "" middle 101 | , last = last 102 | } 103 | 104 | The outcomes of this example are: 105 | 106 | * If the JSON value is not an object the decoder will fail. 107 | * If the value of field `"middle"` is a string, `maybeMiddle` will be `Just string` 108 | * If the value of field `"middle"` is something else, the decoder will fail. 109 | * If the field `"middle"` is missing, `maybeMiddle` will be `Nothing` 110 | 111 | Note that optional is not the same as nullable. If a field must exist but can 112 | be null, use [`require`](#require) and 113 | [`Decode.nullable`](https://package.elm-lang.org/packages/elm/json/latest/Json-Decode#nullable) 114 | instead: 115 | 116 | require "field" (Decode.nullable Decode.string) <| \field -> 117 | 118 | If a field is both optional and nullable [`attempt`](#attempt) is a better 119 | option than using `optional` with `Decode.nullable`, as `attempt` gives you a 120 | `Maybe a` compared to the `Maybe (Maybe a)` that `optional` with `nullable` 121 | would give: 122 | 123 | attempt "field" Decode.string <| \maybeField -> 124 | 125 | -} 126 | optional : String -> Decoder a -> (Maybe a -> Decoder b) -> Decoder b 127 | optional fieldName valueDecoder continuation = 128 | attempt fieldName Decode.value <| \value -> 129 | case value of 130 | Just _ -> 131 | require fieldName valueDecoder (Decode.succeed << Just) 132 | |> Decode.andThen continuation 133 | 134 | Nothing -> 135 | continuation Nothing 136 | 137 | 138 | {-| Decode optional nested fields. Works the same was as `optional` but on nested fields. 139 | 140 | -} 141 | optionalAt : List String -> Decoder a -> (Maybe a -> Decoder b) -> Decoder b 142 | optionalAt path valueDecoder continuation = 143 | attemptAt path Decode.value <| \value -> 144 | case value of 145 | Just _ -> 146 | requireAt path valueDecoder (Decode.succeed << Just) 147 | |> Decode.andThen continuation 148 | 149 | Nothing -> 150 | continuation Nothing 151 | 152 | 153 | {-| Decode fields that may fail. 154 | 155 | Always decodes to a `Maybe` value and never fails. 156 | 157 | Example: 158 | 159 | import Json.Decode as Decode exposing (Decoder) 160 | 161 | person : Decoder Person 162 | person = 163 | require "name" Decode.string <| \name -> 164 | attempt "weight" Decode.int <| \maybeWeight -> 165 | 166 | Decode.succeed 167 | { name = name 168 | , weight = maybeWeight 169 | } 170 | 171 | In this example the `maybeWeight` value will be `Nothing` if: 172 | 173 | * The JSON value was not an object 174 | * The `weight` field is missing. 175 | * The `weight` field is not an `Int`. 176 | 177 | In this case there is no difference between a field being `null` or missing. 178 | 179 | -} 180 | attempt : String -> Decoder a -> (Maybe a -> Decoder b) -> Decoder b 181 | attempt fieldName valueDecoder continuation = 182 | Decode.maybe (Decode.field fieldName valueDecoder) 183 | |> Decode.andThen continuation 184 | 185 | 186 | {-| Decode nested fields that may fail. Works the same way as `attempt` but on nested fields. 187 | 188 | -} 189 | attemptAt : List String -> Decoder a -> (Maybe a -> Decoder b) -> Decoder b 190 | attemptAt path valueDecoder continuation = 191 | Decode.maybe (Decode.at path valueDecoder) 192 | |> Decode.andThen continuation 193 | -------------------------------------------------------------------------------- /src/Json/Decode/Flags.elm: -------------------------------------------------------------------------------- 1 | module Json.Decode.Flags exposing 2 | ( FlagsDecoder, at, return 3 | , Error, decodeString, decodeValue 4 | ) 5 | 6 | {-| 7 | 8 | 9 | # Flags decoder 10 | 11 | This module helps you create a Json Decoder that will never fail. 12 | This is useful when you want to decode a record but not 13 | fail if something is wrong. 14 | 15 | This could of course be achieved by defaulting if the normal decoder fails: 16 | 17 | Json.Decode.decodeValue recordDecoder value 18 | |> Result.withDefault defaultRecord 19 | 20 | The problem with this approach is that if one field is faulty the 21 | whole record will be defaulted. In some cases you want to decode 22 | everything possible and only use default for fields that couldn't 23 | be decoded. 24 | 25 | This decoder will always succeed with a value and a list of errors. 26 | 27 | ``` 28 | import Json.Decode as Decode 29 | 30 | decoder = 31 | at ["field1"] Decode.string "Default 1" <| \value1 -> 32 | at ["field2"] Decode.string "Default 2" <| \value2 -> 33 | return 34 | { field1 = value1 35 | , field2 = value2 36 | } 37 | ``` 38 | 39 | Running the decoder with this Json value: 40 | 41 | ``` 42 | { 43 | "field1": "Hello", 44 | "field2": null 45 | } 46 | ``` 47 | 48 | Will result in the record: 49 | 50 | ``` 51 | { field1 = "Hello" 52 | , field2 = "Default 2" 53 | } 54 | ``` 55 | 56 | and a list of `Error`: 57 | 58 | ``` 59 | [ { path = ["field2"] 60 | , error = Field "field2" (Failure ("Expecting a STRING") ) 61 | } 62 | ] 63 | ``` 64 | 65 | ## Create a Flags Decoder 66 | 67 | @docs FlagsDecoder, at, return 68 | 69 | 70 | ## Run FlagsDecoder 71 | 72 | @docs Error, decodeString, decodeValue 73 | 74 | -} 75 | 76 | import Json.Decode as Decode 77 | 78 | 79 | type alias JsonDecoder a = 80 | Decode.Decoder a 81 | 82 | 83 | {-| A decoder that never fails. 84 | -} 85 | type FlagsDecoder a 86 | = FlagsDecoder (JsonDecoder ( List Error, a )) 87 | 88 | 89 | {-| A decode error. 90 | -} 91 | type alias Error = 92 | { path : List String 93 | , error : Decode.Error 94 | } 95 | 96 | 97 | type alias TestRecord = 98 | { field1 : String 99 | , field2 : String 100 | } 101 | 102 | 103 | test = 104 | let 105 | decoder : FlagsDecoder TestRecord 106 | decoder = 107 | at ["field1"] Decode.string "Default 1" <| \value1 -> 108 | at ["field2"] Decode.string "Default 2" <| \value2 -> 109 | return 110 | { field1 = value1 111 | , field2 = value2 112 | } 113 | 114 | json = 115 | """ 116 | { "field1": "Hello" 117 | , "field2": null 118 | } 119 | """ 120 | in 121 | decodeString decoder json 122 | 123 | 124 | {-| Decode a field with an optional value 125 | 126 | at 127 | 128 | at ["field1"] Decode.string "1" <| \value1 -> 129 | at ["field2"] Decode.int 2 <| \value2 -> 130 | return 131 | { field1 = value1 132 | , field2 = value2 133 | } 134 | -} 135 | at : List String -> JsonDecoder a -> a -> (a -> FlagsDecoder b) -> FlagsDecoder b 136 | at path decoder defaultValue continuation = 137 | Decode.value 138 | |> Decode.andThen 139 | (\jsonValue -> 140 | let 141 | fieldDecoder = 142 | Decode.at path decoder 143 | in 144 | case Decode.decodeValue fieldDecoder jsonValue of 145 | Ok value -> 146 | let 147 | (FlagsDecoder result) = 148 | continuation value 149 | in 150 | result 151 | 152 | Err decodeError -> 153 | let 154 | (FlagsDecoder result) = 155 | continuation defaultValue 156 | in 157 | result 158 | |> Decode.map 159 | (\( errors, value ) -> 160 | ( { path = path 161 | , error = decodeError 162 | } 163 | :: errors 164 | , value 165 | ) 166 | ) 167 | ) 168 | |> FlagsDecoder 169 | 170 | 171 | {-| Return a value from your decoder. 172 | -} 173 | return : a -> FlagsDecoder a 174 | return arg = 175 | Decode.succeed 176 | ( [] 177 | , arg 178 | ) 179 | |> FlagsDecoder 180 | 181 | 182 | {-| Decode a json string 183 | -} 184 | decodeString : FlagsDecoder a -> String -> ( List Error, a ) 185 | decodeString (FlagsDecoder decoder) value = 186 | case Decode.decodeString decoder value of 187 | Ok v -> 188 | v 189 | 190 | Err e -> 191 | -- This decoder can never fail so this will never happen (hopefully). 192 | decodeString (FlagsDecoder decoder) value 193 | 194 | 195 | {-| Decode a Json value 196 | -} 197 | decodeValue : FlagsDecoder a -> Decode.Value -> ( List Error, a ) 198 | decodeValue (FlagsDecoder decoder) value = 199 | case Decode.decodeValue decoder value of 200 | Ok v -> 201 | v 202 | 203 | Err e -> 204 | -- This decoder can never fail so this will never happen (I hope). 205 | decodeValue (FlagsDecoder decoder) value 206 | --------------------------------------------------------------------------------