├── .travis.yml ├── .gitignore ├── tests └── elm-verify-examples.json ├── package.json ├── src └── Json │ ├── Encode │ └── Extra.elm │ └── Decode │ └── Extra.elm ├── elm.json ├── README.md ├── LICENSE ├── docs └── andMap.md └── CHANGELOG.md /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: "node" 4 | os: linux 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore build or dist files 2 | elm-stuff 3 | node_modules 4 | /tests/VerifyExamples 5 | -------------------------------------------------------------------------------- /tests/elm-verify-examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "../src", 3 | "tests": [ 4 | "Json.Encode.Extra", 5 | "Json.Decode.Extra" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "elm-format --validate src && elm-verify-examples && elm-test" 4 | }, 5 | "dependencies": { 6 | "elm": "0.19.0-no-deps", 7 | "elm-format": "^0.8.2", 8 | "elm-verify-examples": "^4.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Json/Encode/Extra.elm: -------------------------------------------------------------------------------- 1 | module Json.Encode.Extra exposing (maybe) 2 | 3 | {-| Convenience functions for turning Elm values into Json values. 4 | 5 | @docs maybe 6 | 7 | -} 8 | 9 | import Json.Encode exposing (Value, encode, int, null, object) 10 | 11 | 12 | {-| Encode a Maybe value. If the value is `Nothing` it will be encoded as `null` 13 | 14 | import Json.Encode exposing (..) 15 | 16 | 17 | maybe int (Just 50) 18 | --> int 50 19 | 20 | 21 | maybe int Nothing 22 | --> null 23 | 24 | -} 25 | maybe : (a -> Value) -> Maybe a -> Value 26 | maybe encoder = 27 | Maybe.map encoder >> Maybe.withDefault null 28 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "elm-community/json-extra", 4 | "summary": "Convenience functions for working with JSON", 5 | "license": "MIT", 6 | "version": "4.3.0", 7 | "exposed-modules": [ 8 | "Json.Decode.Extra", 9 | "Json.Encode.Extra" 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 | "elm/time": "1.0.0 <= v < 2.0.0", 16 | "elm/url": "1.0.0 <= v < 2.0.0", 17 | "rtfeldman/elm-iso8601-date-strings": "1.0.0 <= v < 2.0.0" 18 | }, 19 | "test-dependencies": { 20 | "elm-explorations/test": "1.0.0 <= v < 2.0.0" 21 | } 22 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/elm-community/json-extra.svg?branch=master)](https://travis-ci.org/elm-community/json-extra) 2 | 3 | # json-extra 4 | 5 | ``` 6 | elm install elm-community/json-extra 7 | ``` 8 | 9 | Convenience functions for working with JSON. Decoders in this package cover 10 | reasonably common, and slightly less common use-cases you may encounter when 11 | working with JSON in Elm. 12 | 13 | ## Contributing 14 | 15 | Contributions are always welcome. Before opening a PR, it's generally a good 16 | idea to open an issue first, so we can discuss the use-cases before diving into 17 | the code. 18 | 19 | ## Upgrading to Elm 0.19 20 | 21 | `date` is now `datetime` and only sucessfully decodes strings in Iso 8601 format. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 CircuitHub Inc., Elm Community members 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /docs/andMap.md: -------------------------------------------------------------------------------- 1 | ## Json.Decode.Extra.andMap 2 | 3 | Imagine you have a data type for a user 4 | 5 | ```elm 6 | import Date (Date) 7 | 8 | type alias User = 9 | { id : Int 10 | , createdAt : Date 11 | , updatedAt : Date 12 | , deletedAt : Maybe Date 13 | , username : Maybe String 14 | , email : Maybe String 15 | , isAdmin : Bool 16 | } 17 | ``` 18 | 19 | You can use `andMap` to incrementally apply decoders to your `User` type alias 20 | by using that type alias as a function. Recall that record type aliases are 21 | also functions which accept arguments in the order their fields are declared. In 22 | this case, `User` looks like 23 | 24 | ```elm 25 | User : Int -> Date -> Date -> Maybe Date -> Maybe String -> Maybe String -> Bool -> User 26 | ``` 27 | 28 | And also recall that Elm functions can be partially applied. We can use these 29 | properties to apply each field of our JSON object to each field in our user one 30 | field at a time. All we need to do is also wrap `User` in a decoder and step 31 | through using `andMap`. 32 | 33 | ```elm 34 | userDecoder : Decoder User 35 | userDecoder = 36 | succeed User 37 | |> andMap (field "id" int) 38 | |> andMap (field "createdAt" date) 39 | |> andMap (field "updatedAt" date) 40 | |> andMap (field "deletedAt" (maybe date)) 41 | |> andMap (field "username" (maybe string)) 42 | |> andMap (field "email" (maybe string)) 43 | |> andMap (field "isAdmin" bool) 44 | ``` 45 | 46 | This is a shortened form of 47 | 48 | ```elm 49 | userDecoder : Decoder User 50 | userDecoder = 51 | succeed User 52 | |> andThen (\f -> map f (field "id" int)) 53 | |> andThen (\f -> map f (field "createdAt" date)) 54 | |> andThen (\f -> map f (field "updatedAt" date)) 55 | |> andThen (\f -> map f (field "deletedAt" (maybe date))) 56 | |> andThen (\f -> map f (field "username" (maybe string))) 57 | |> andThen (\f -> map f (field "email" (maybe string))) 58 | |> andThen (\f -> map f (field "isAdmin" bool)) 59 | ``` 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 4.3.0 2 | 3 | **Additions:** 4 | - `fromMaybe`: Like `fromResult` but for `Maybe a` 5 | 6 | ### 4.2.0 7 | 8 | **Additions:** 9 | - `optionalNullableField`: Putting `optionalField` and `nullable` together 10 | 11 | ### 4.1.0 12 | 13 | **Additions:** 14 | - `url`: Decode JSON strings to `elm/url#Url`s 15 | 16 | ### 4.0.0 17 | 18 | **Removals:** 19 | - `dict`: `elm/json` already comes with a `dict` encoder 20 | 21 | **Additions:** 22 | - `datetime`: Re-introduce decoding a date+time, to `Posix` 23 | 24 | **Changes:** 25 | - doc tests are re-enabled 26 | - docs are fixed 27 | 28 | ### 3.0.0 29 | 30 | **Removals** 31 | - `date`: Initial 0.19 compatibility means (temporarily) removing the `date** decoder 32 | 33 | **Other stuff:** 34 | - Elm 0.19 support! 35 | - doc tests are disabled for now 36 | 37 | ### 2.6.0 38 | 39 | **Additions:** 40 | - `keys` allows extracting _only_ the keys from a JSON object 41 | 42 | ### 2.5.0 43 | 44 | **Additions:** 45 | - `dict` helps encoding `Dict` 46 | 47 | ### 2.4.0 48 | 49 | **Additions:** 50 | - `collection` helps with decoding array-like JavaScript structures such as `HTMLCollection` 51 | - `combine` helps combining a `List` of decoders into a single `Decoder` for a `List` of such things 52 | 53 | ### 2.3.0 54 | 55 | **Additions:** 56 | - `indexedList` to get access to the current js array index while decoding 57 | 58 | **Other Stuff:** 59 | - `elm-doc-test` is now `elm-verify-examples`! 60 | 61 | ### 2.2.0 62 | 63 | **Additions:** 64 | - `parseInt` and `parseFloat` for weird api's that return numbers as strings 65 | - `doubleEncoded` for a more generic _json as a string in json_ issues 66 | 67 | **Fixes:** 68 | - `optionalField` decodes the field, rather than the surrounding object now. 69 | 70 | **Other Stuff:** 71 | - Code Style conforms to elm-format@exp 72 | - Doc tests! 73 | - Travis integration 74 | 75 | ### 2.1.0 76 | 77 | **Additions:** 78 | - `optionalField : String -> Json.Decode.Decoder a -> Json.Decode.Decoder (Maybe.Maybe a)` - Decode an optional field, succeeding with `Nothing` if it is missing, but still giving an error if it is malformed. 79 | 80 | ### 2.0.0 81 | 82 | **Breaking Changes:** 83 | - Upgrade for Elm 0.18 84 | - Removed `maybeNull` in favor of `Json.Decode.nullable` 85 | - Removed `lazy` in favor of `Json.Decode.lazy` 86 | - Renamed `apply` to `andMap` and reversed arguments to `Decoder a -> Decoder (a -> b) -> Decoder b` to make it work nicely with `(|>)` 87 | 88 | **Additions:** 89 | - `fromResult : Result String a -> Decoder a` - convert a `Result` to a `Decoder`, helpful in `andThen` callbacks following the removal of `Json.Decode.customDecoder` 90 | - `Json.Encode.Extra.maybe : (a -> Value) -> Maybe a -> Value` - encode a `Maybe a` given an encoder for `a`. Thanks to @hendore for this addition. 91 | 92 | **Other Stuff:** 93 | - Code style conforms to elm-format 94 | 95 | #### 1.1.0 96 | 97 | **Additions:** 98 | - `Json.Decode.Extra.sequence` - lets you generate a list of `Decoder a` and attempt to apply them to a JSON list. _Authored by @cobalamin_ 99 | 100 | 101 | #### 1.0.0 102 | 103 | **Breaking Changes:** 104 | - Upgrade for Elm 0.17 105 | -------------------------------------------------------------------------------- /src/Json/Decode/Extra.elm: -------------------------------------------------------------------------------- 1 | module Json.Decode.Extra exposing 2 | ( datetime 3 | , url 4 | , andMap 5 | , when 6 | , collection, sequence, combine, indexedList, keys 7 | , set 8 | , dict2 9 | , withDefault, optionalField, optionalNullableField, fromMaybe 10 | , fromResult 11 | , parseInt, parseFloat, doubleEncoded 12 | ) 13 | 14 | {-| Convenience functions for working with Json 15 | 16 | 17 | # Dates 18 | 19 | @docs datetime 20 | 21 | 22 | # URLs 23 | 24 | @docs url 25 | 26 | 27 | # Incremental Decoding 28 | 29 | @docs andMap 30 | 31 | 32 | # Conditional Decoding 33 | 34 | @docs when 35 | 36 | 37 | # List 38 | 39 | @docs collection, sequence, combine, indexedList, keys 40 | 41 | 42 | # Set 43 | 44 | @docs set 45 | 46 | 47 | # Dict 48 | 49 | @docs dict2 50 | 51 | 52 | # Maybe 53 | 54 | @docs withDefault, optionalField, optionalNullableField, fromMaybe 55 | 56 | 57 | # Result 58 | 59 | @docs fromResult 60 | 61 | 62 | # Encoded strings 63 | 64 | @docs parseInt, parseFloat, doubleEncoded 65 | 66 | -} 67 | 68 | import Dict exposing (Dict) 69 | import Iso8601 70 | import Json.Decode exposing (..) 71 | import Set exposing (Set) 72 | import Time 73 | import Url 74 | 75 | 76 | {-| Decode an ISO-8601 formatted date-time string. 77 | 78 | This always returns a `Time.Posix` value, which is naturally always expressed in 79 | UTC. 80 | 81 | import Json.Decode exposing (..) 82 | import Json.Encode 83 | import Time 84 | 85 | """ "2018-08-26T09:46:00+02:00" """ 86 | |> decodeString datetime 87 | --> Ok (Time.millisToPosix 1535269560000) 88 | 89 | """ "" """ 90 | |> decodeString datetime 91 | --> Err 92 | --> (Failure 93 | --> "Expecting an ISO-8601 formatted date+time string" 94 | --> (Json.Encode.string "") 95 | --> ) 96 | 97 | -} 98 | datetime : Decoder Time.Posix 99 | datetime = 100 | andThen 101 | (\dateString -> 102 | case Iso8601.toTime dateString of 103 | Ok v -> 104 | succeed v 105 | 106 | Err _ -> 107 | fail "Expecting an ISO-8601 formatted date+time string" 108 | ) 109 | string 110 | 111 | 112 | {-| Decode a URL 113 | 114 | This always returns a `Url.Url` value. 115 | 116 | import Json.Decode exposing (..) 117 | import Url 118 | 119 | """ "http://foo.bar/quux" """ 120 | |> decodeString url 121 | --> Ok <| Url.Url Url.Http "foo.bar" Nothing "/quux" Nothing Nothing 122 | 123 | -} 124 | url : Decoder Url.Url 125 | url = 126 | andThen 127 | (\urlString -> 128 | case Url.fromString urlString of 129 | Just actualUrl -> 130 | succeed actualUrl 131 | 132 | Nothing -> 133 | fail "Expecting a URL" 134 | ) 135 | string 136 | 137 | 138 | {-| Can be helpful when decoding large objects incrementally. 139 | 140 | See [the `andMap` docs](https://github.com/elm-community/json-extra/blob/2.0.0/docs/andMap.md) 141 | for an explanation of how `andMap` works and how to use it. 142 | 143 | -} 144 | andMap : Decoder a -> Decoder (a -> b) -> Decoder b 145 | andMap = 146 | map2 (|>) 147 | 148 | 149 | {-| Extract a set. 150 | 151 | import Json.Decode exposing (..) 152 | import Set 153 | 154 | 155 | "[ 1, 1, 5, 2 ]" 156 | |> decodeString (set int) 157 | --> Ok <| Set.fromList [ 1, 2, 5 ] 158 | 159 | -} 160 | set : Decoder comparable -> Decoder (Set comparable) 161 | set decoder = 162 | list decoder 163 | |> map Set.fromList 164 | 165 | 166 | {-| Extract a dict using separate decoders for keys and values. 167 | 168 | import Json.Decode exposing (..) 169 | import Dict 170 | 171 | 172 | """ { "1": "foo", "2": "bar" } """ 173 | |> decodeString (dict2 int string) 174 | --> Ok <| Dict.fromList [ ( 1, "foo" ), ( 2, "bar" ) ] 175 | 176 | -} 177 | dict2 : Decoder comparable -> Decoder v -> Decoder (Dict comparable v) 178 | dict2 keyDecoder valueDecoder = 179 | keyValuePairs valueDecoder 180 | |> andThen (decodeDictFromTuples keyDecoder) 181 | 182 | 183 | {-| Helper function for dict 184 | -} 185 | decodeDictFromTuples : Decoder comparable -> List ( String, v ) -> Decoder (Dict comparable v) 186 | decodeDictFromTuples keyDecoder tuples = 187 | case tuples of 188 | [] -> 189 | succeed Dict.empty 190 | 191 | ( strKey, value ) :: rest -> 192 | case decodeString keyDecoder strKey of 193 | Ok key -> 194 | decodeDictFromTuples keyDecoder rest 195 | |> andThen (Dict.insert key value >> succeed) 196 | 197 | Err error -> 198 | fail (errorToString error) 199 | 200 | 201 | {-| Try running the given decoder; if that fails, then succeed with the given 202 | fallback value. 203 | 204 | import Json.Decode exposing (..) 205 | 206 | 207 | """ { "children": "oops" } """ 208 | |> decodeString (field "children" (list string) |> withDefault []) 209 | --> Ok [] 210 | 211 | 212 | """ null """ 213 | |> decodeString (field "children" (list string) |> withDefault []) 214 | --> Ok [] 215 | 216 | 217 | """ 30 """ 218 | |> decodeString (int |> withDefault 42) 219 | --> Ok 30 220 | 221 | -} 222 | withDefault : a -> Decoder a -> Decoder a 223 | withDefault fallback decoder = 224 | maybe decoder 225 | |> map (Maybe.withDefault fallback) 226 | 227 | 228 | {-| If a field is missing, succeed with `Nothing`. If it is present, decode it 229 | as normal and wrap successes in a `Just`. 230 | 231 | When decoding with 232 | [`maybe`](http://package.elm-lang.org/packages/elm-lang/core/latest/Json-Decode#maybe), 233 | if a field is present but malformed, you get a success and Nothing. 234 | `optionalField` gives you a failed decoding in that case, so you know 235 | you received malformed data. 236 | 237 | Examples: 238 | 239 | import Json.Decode exposing (..) 240 | import Json.Encode 241 | 242 | Let's define a `stuffDecoder` that extracts the `"stuff"` field, if it exists. 243 | 244 | stuffDecoder : Decoder (Maybe String) 245 | stuffDecoder = 246 | optionalField "stuff" string 247 | 248 | If the "stuff" field is missing, decode to Nothing. 249 | 250 | """ { } """ 251 | |> decodeString stuffDecoder 252 | --> Ok Nothing 253 | 254 | If the "stuff" field is present but not a String, fail decoding. 255 | 256 | expectedError : Error 257 | expectedError = 258 | Failure "Expecting a STRING" (Json.Encode.list identity []) 259 | |> Field "stuff" 260 | 261 | """ { "stuff": [] } """ 262 | |> decodeString stuffDecoder 263 | --> Err expectedError 264 | 265 | If the "stuff" field is present and valid, decode to Just String. 266 | 267 | """ { "stuff": "yay!" } """ 268 | |> decodeString stuffDecoder 269 | --> Ok <| Just "yay!" 270 | 271 | -} 272 | optionalField : String -> Decoder a -> Decoder (Maybe a) 273 | optionalField fieldName decoder = 274 | let 275 | finishDecoding json = 276 | case decodeValue (field fieldName value) json of 277 | Ok val -> 278 | -- The field is present, so run the decoder on it. 279 | map Just (field fieldName decoder) 280 | 281 | Err _ -> 282 | -- The field was missing, which is fine! 283 | succeed Nothing 284 | in 285 | value 286 | |> andThen finishDecoding 287 | 288 | 289 | {-| A neat combination of `optionalField` and `nullable`. 290 | 291 | What this means is that a decoder like `optionalNullableField "foo" string` will 292 | return `Just "hello"` for `{"foo": "hello"}`, `Nothing` for both `{"foo": null}` 293 | and `{}`, and an error for malformed input like `{"foo": 123}`. 294 | 295 | import Json.Decode exposing (Decoder, Error(..), decodeString, field, string) 296 | import Json.Decode.Extra exposing (optionalNullableField) 297 | import Json.Encode 298 | 299 | myDecoder : Decoder (Maybe String) 300 | myDecoder = 301 | optionalNullableField "foo" string 302 | 303 | 304 | """ {"foo": "hello"} """ 305 | |> decodeString myDecoder 306 | --> Ok (Just "hello") 307 | 308 | 309 | """ {"foo": null} """ 310 | |> decodeString myDecoder 311 | --> Ok Nothing 312 | 313 | 314 | """ {} """ 315 | |> decodeString myDecoder 316 | --> Ok Nothing 317 | 318 | 319 | """ {"foo": 123} """ 320 | |> decodeString myDecoder 321 | |> Result.mapError (\_ -> "expected error") 322 | --> Err "expected error" 323 | 324 | -} 325 | optionalNullableField : String -> Decoder a -> Decoder (Maybe a) 326 | optionalNullableField fieldName decoder = 327 | map (Maybe.andThen identity) 328 | (optionalField fieldName (nullable decoder)) 329 | 330 | 331 | {-| This function turns a list of decoders into a decoder that returns a list. 332 | 333 | The returned decoder will zip the list of decoders with a list of values, 334 | matching each decoder with exactly one value at the same position. This is most 335 | often useful in cases when you find yourself needing to dynamically generate a 336 | list of decoders based on some data, and decode some other data with this list 337 | of decoders. 338 | 339 | Note that this function, unlike `List.map2`'s behaviour, expects the list of 340 | decoders to have the same length as the list of values in the JSON. 341 | 342 | import Json.Decode exposing (..) 343 | 344 | 345 | decoder : Decoder (List (Maybe String)) 346 | decoder = 347 | sequence 348 | [ map Just string 349 | , succeed Nothing 350 | , map Just string 351 | ] 352 | 353 | 354 | decodeString decoder """ [ "pick me", "ignore me", "and pick me" ] """ 355 | --> Ok [ Just "pick me", Nothing, Just "and pick me" ] 356 | 357 | -} 358 | sequence : List (Decoder a) -> Decoder (List a) 359 | sequence decoders = 360 | decoders 361 | |> List.indexedMap (\idx dec -> index idx dec) 362 | |> combine 363 | 364 | 365 | {-| Get access to the current index while decoding a list element. 366 | 367 | import Json.Decode exposing (..) 368 | 369 | 370 | repeatedStringDecoder : Int -> Decoder String 371 | repeatedStringDecoder times = 372 | string |> map (String.repeat times) 373 | 374 | 375 | """ [ "a", "b", "c", "d" ] """ 376 | |> decodeString (indexedList repeatedStringDecoder) 377 | --> Ok [ "", "b", "cc", "ddd" ] 378 | 379 | -} 380 | indexedList : (Int -> Decoder a) -> Decoder (List a) 381 | indexedList indexedDecoder = 382 | list value 383 | |> andThen 384 | (\values -> 385 | List.range 0 (List.length values - 1) 386 | |> List.map indexedDecoder 387 | |> sequence 388 | ) 389 | 390 | 391 | {-| Get a list of the keys of a JSON object 392 | 393 | import Json.Decode exposing (..) 394 | 395 | 396 | """ { "alice": 42, "bob": 99 } """ 397 | |> decodeString keys 398 | --> Ok [ "alice", "bob" ] 399 | 400 | -} 401 | keys : Decoder (List String) 402 | keys = 403 | keyValuePairs (succeed ()) 404 | |> map (List.map Tuple.first) 405 | 406 | 407 | {-| Transform a `Maybe a` into a `Decoder a` 408 | 409 | Sometimes, you'll have a function that produces a `Maybe a` value, that you may 410 | want to use in a decoder. 411 | 412 | Let's say, for example, that we have a function to extract the first letter of a 413 | string, and we want to use that in a decoder so we can extract only the first 414 | letter of that string. 415 | 416 | import Json.Decode exposing (..) 417 | import Json.Encode 418 | 419 | 420 | firstLetter : String -> Maybe Char 421 | firstLetter input = 422 | Maybe.map Tuple.first (String.uncons input) 423 | 424 | 425 | firstLetterDecoder : Decoder Char 426 | firstLetterDecoder = 427 | andThen 428 | (fromMaybe "Empty string not allowed" << firstLetter) 429 | string 430 | 431 | """ "something" """ 432 | |> decodeString firstLetterDecoder 433 | --> Ok 's' 434 | 435 | 436 | """ "" """ 437 | |> decodeString firstLetterDecoder 438 | --> Err (Failure "Empty string not allowed" (Json.Encode.string "")) 439 | 440 | -} 441 | fromMaybe : String -> Maybe a -> Decoder a 442 | fromMaybe error val = 443 | case val of 444 | Just v -> 445 | succeed v 446 | 447 | Nothing -> 448 | fail error 449 | 450 | 451 | {-| Transform a result into a decoder 452 | 453 | Sometimes it can be useful to use functions that primarily operate on 454 | `Result` in decoders. 455 | 456 | import Json.Decode exposing (..) 457 | import Json.Encode 458 | 459 | 460 | validateString : String -> Result String String 461 | validateString input = 462 | case input of 463 | "" -> 464 | Err "Empty string is not allowed" 465 | _ -> 466 | Ok input 467 | 468 | 469 | """ "something" """ 470 | |> decodeString (string |> andThen (fromResult << validateString)) 471 | --> Ok "something" 472 | 473 | 474 | """ "" """ 475 | |> decodeString (string |> andThen (fromResult << validateString)) 476 | --> Err (Failure "Empty string is not allowed" (Json.Encode.string "")) 477 | 478 | -} 479 | fromResult : Result String a -> Decoder a 480 | fromResult result = 481 | case result of 482 | Ok successValue -> 483 | succeed successValue 484 | 485 | Err errorMessage -> 486 | fail errorMessage 487 | 488 | 489 | {-| Extract an int using [`String.toInt`](http://package.elm-lang.org/packages/elm-lang/core/latest/String#toInt) 490 | 491 | import Json.Decode exposing (..) 492 | 493 | 494 | """ { "field": "123" } """ 495 | |> decodeString (field "field" parseInt) 496 | --> Ok 123 497 | 498 | -} 499 | parseInt : Decoder Int 500 | parseInt = 501 | string |> andThen (String.toInt >> fromMaybe "Failed to parse as int") 502 | 503 | 504 | {-| Extract a float using [`String.toFloat`](http://package.elm-lang.org/packages/elm-lang/core/latest/String#toFloat) 505 | 506 | import Json.Decode exposing (..) 507 | 508 | 509 | """ { "field": "50.5" } """ 510 | |> decodeString (field "field" parseFloat) 511 | --> Ok 50.5 512 | 513 | -} 514 | parseFloat : Decoder Float 515 | parseFloat = 516 | string |> andThen (String.toFloat >> fromMaybe "failed to parse as float") 517 | 518 | 519 | {-| Extract a JSON-encoded string field 520 | 521 | "Yo dawg, I heard you like JSON..." 522 | 523 | If someone has put JSON in your JSON (perhaps a JSON log entry, encoded 524 | as a string) this is the function you're looking for. Give it a decoder 525 | and it will return a new decoder that applies your decoder to a string 526 | field and yields the result (or fails if your decoder fails). 527 | 528 | import Json.Decode exposing (..) 529 | 530 | 531 | logEntriesDecoder : Decoder (List String) 532 | logEntriesDecoder = 533 | doubleEncoded (list string) 534 | 535 | 536 | logsDecoder : Decoder (List String) 537 | logsDecoder = 538 | field "logs" logEntriesDecoder 539 | 540 | 541 | """ { "logs": "[\\"log1\\", \\"log2\\"]"} """ 542 | |> decodeString logsDecoder 543 | --> Ok [ "log1", "log2" ] 544 | 545 | -} 546 | doubleEncoded : Decoder a -> Decoder a 547 | doubleEncoded decoder = 548 | string 549 | |> andThen 550 | (fromResult 551 | << Result.mapError errorToString 552 | << decodeString decoder 553 | ) 554 | 555 | 556 | {-| Helps converting a list of decoders into a decoder for a list of that type. 557 | 558 | import Json.Decode exposing (..) 559 | 560 | 561 | decoders : List (Decoder String) 562 | decoders = 563 | [ field "foo" string 564 | , field "bar" string 565 | , field "another" string 566 | ] 567 | 568 | 569 | """ { "foo": "hello", "another": "!", "bar": "world" } """ 570 | |> decodeString (combine decoders) 571 | --> Ok [ "hello", "world", "!" ] 572 | 573 | -} 574 | combine : List (Decoder a) -> Decoder (List a) 575 | combine = 576 | List.foldr (map2 (::)) (succeed []) 577 | 578 | 579 | {-| Some JavaScript structures look like arrays, but aren't really. Examples 580 | include `HTMLCollection`, `NodeList` and everything else that has a `length` 581 | property, has values indexed by an integer key between 0 and `length`, but yet 582 | _is not_ a JavaScript Array. 583 | 584 | This decoder can come to the rescue. 585 | 586 | import Json.Decode exposing (..) 587 | 588 | 589 | """ { "length": 3, "0": "foo", "1": "bar", "2": "baz" } """ 590 | |> decodeString (collection string) 591 | --> Ok [ "foo", "bar", "baz" ] 592 | 593 | -} 594 | collection : Decoder a -> Decoder (List a) 595 | collection decoder = 596 | field "length" int 597 | |> andThen 598 | (\length -> 599 | List.range 0 (length - 1) 600 | |> List.map (\index -> field (String.fromInt index) decoder) 601 | |> combine 602 | ) 603 | 604 | 605 | {-| Helper for conditionally decoding values based on some discriminator 606 | that needs to pass a certain check. 607 | 608 | import Json.Decode exposing (..) 609 | import Json.Encode 610 | 611 | 612 | is : a -> a -> Bool 613 | is a b = 614 | a == b 615 | 616 | 617 | enabledValue : Decoder Int 618 | enabledValue = 619 | (field "value" int) 620 | |> when (field "enabled" bool) (is True) 621 | 622 | 623 | """ { "enabled": true, "value": 123 } """ 624 | |> decodeString enabledValue 625 | --> Ok 123 626 | 627 | 628 | input : Json.Decode.Value 629 | input = 630 | Json.Encode.object 631 | [ ( "enabled", Json.Encode.bool False ) 632 | , ( "value", Json.Encode.int 321 ) 633 | ] 634 | 635 | expectedError : Error 636 | expectedError = 637 | Failure "Check failed with input" input 638 | 639 | """ { "enabled": false, "value": 321 } """ 640 | |> decodeString enabledValue 641 | --> Err expectedError 642 | 643 | This can also be used to decode union types that are encoded with a discriminator field: 644 | 645 | type Animal = Cat String | Dog String 646 | 647 | 648 | dog : Decoder Animal 649 | dog = 650 | map Dog (field "name" string) 651 | 652 | 653 | cat : Decoder Animal 654 | cat = 655 | map Cat (field "name" string) 656 | 657 | 658 | animalType : Decoder String 659 | animalType = 660 | field "type" string 661 | 662 | 663 | animal : Decoder Animal 664 | animal = 665 | oneOf 666 | [ when animalType (is "dog") dog 667 | , when animalType (is "cat") cat 668 | ] 669 | 670 | 671 | """ 672 | [ 673 | { "type": "dog", "name": "Dawg" }, 674 | { "type": "cat", "name": "Roxy" } 675 | ] 676 | """ 677 | |> decodeString (list animal) 678 | --> Ok [ Dog "Dawg", Cat "Roxy" ] 679 | 680 | -} 681 | when : Decoder a -> (a -> Bool) -> Decoder b -> Decoder b 682 | when checkDecoder check passDecoder = 683 | andThen 684 | (\checkVal -> 685 | if check checkVal then 686 | passDecoder 687 | 688 | else 689 | fail "Check failed with input" 690 | ) 691 | checkDecoder 692 | --------------------------------------------------------------------------------