The Author Online Book Forums are Moving

The Author Online Book Forums will soon redirect to Manning's liveBook and liveVideo. All book forum content will migrate to liveBook's discussion forum and all video forum content will migrate to liveVideo. Log in to liveBook or liveVideo with your Manning credentials to join the discussion!

Thank you for your engagement in the AoF over the years! We look forward to offering you a more enhanced forum experience.

tempusfugit (144) [Avatar] Offline
#1
Sure, but that isn't an obstacle as Evan Czaplicki alludes to in this post. Basically Decoders are composable (because Decoders are applicative functors (I assume by design)). So a contrived example:

photoDecoder : Decoder Photo
photoDecoder =
    Json.Decode.map3
        Photo
        (field "url" string)
        (field "size" int)
        (oneOf [(field "title" string), (succeed "untitled")])


is trivially rewritten as:

photoDecoder : Decoder Photo
photoDecoder =
    let
        decoder =
            Json.Decode.map3
                Photo
                (field "url" string)
                (field "size" int)
                (oneOf [(field "title" string), (succeed "untitled")]) 
    in
      decoder


which can be replaced with

photoDecoder : Decoder Photo
photoDecoder =
    let
        stage1 = 
            Json.Decode.map2
                Photo
                (field "url" string)
                (field "size" int)
                    
        decoder =
            Json.Decode.map2
                (<|)
                stage1
                (oneOf [(field "title" string), (succeed "untitled")]) 
    in
      decoder


or even

photoDecoder : Decoder Photo
photoDecoder =
    let
        stage1 = 
            Json.Decode.map
                Photo
                (field "url" string)
                    
        stage2 = 
            Json.Decode.map2
                (<|)
                stage1
                (field "size" int)
                    
        decoder =
            Json.Decode.map2
                (<|)
                stage2
                (oneOf [(field "title" string), (succeed "untitled")]) 
    in
      decoder


So, for example, 16 fields can be easily decoded by composing 3 decoders, two of which are created with map8 and one with map2 (or three decoders created with map6) - without having to resort to any additional dependencies.


Furthermore Json.Decode.at and <| (backward function application) are useful for constructing decoders that need to get at nested data.

In general it is useful to know how to compose Decoders in order to extract data from arbitrarily complex structures and into potentially differently shaped target structures, e.g.:

module Main exposing (..) -- Main.elm
import Html exposing (..)
import String
import Date
import Json.Decode exposing
    ( Decoder
    , field
    , string
    , int
    , float
    , bool
    , maybe
    , list
    , at
    , andThen
    , succeed
    , fail
    )

view : String -> Html b
view model =
    div
      []
      [ text model ]

{-
    Target Elm types (concrete records)
-}
type alias Nested =
    { propN1 : String
    , propN2 : Int
    , propN3 : Float           
    }
    
type alias Subject =
    { prop01 : String
    , prop02 : Int
    , prop03 : Float
    , prop04 : Bool
    , prop05 : Maybe Int
    , prop06 : Maybe Date.Date
    , prop07 : List String
    , prop08 : Int        -- only extracting one piece we are interested in
    , prop09 : Nested
    , prop101112 : Nested -- here we are shoving some properties down into their own nested record
    }

{-
    JSON "test subject"
-}

-- List of property names and associated content to be included in the JSON string
properties : List (String, String)
properties = [
        ( "prop01", "\"string01\"" )
    ,   ( "prop02", "9007199254740991" )
    ,   ( "prop03", (toString pi))
    ,   ( "prop04", "true")
    ,   ( "prop06", "\"2016-08-02T13:42:13.272Z\"")
    ,   ( "prop07", "[\"first\", \"second\",\"third\",\"fourth\",\"fifth\",\"sixth\"]")
    ,   ( "prop08", "{ \"unimportant\" : true, \"important\" : 911 }")         
    ,   ( "prop09", "{ \"propN1\" : \"prop09.propN1\", \"propN2\" : -2, \"propN3\" : -3.1  }")
    ,   ( "prop10", "\"prop10\"" )
    ,   ( "prop11", "1" )
    ,   ( "prop12", "1.2")
    ]

-- Turns a property/value list into a "JSON string" 
toJsonString : List (String, String) -> String
toJsonString propList =
    let
        properties = 
            propList
            |> List.map ( \(k,v) -> "\"" ++ k ++ "\" : " ++ v )
            |> List.intersperse ", "
            |> String.concat
    in
        "{ " ++ properties ++ " }"

-- the JSON string with the above "properties"            
jsonSubject : String    
jsonSubject = toJsonString properties


{-
  Decoders & helpers
-}
maybeStringDateDecoder : Maybe String -> Decoder ( Maybe Date.Date )
maybeStringDateDecoder maybeString =
    case maybeString of
        Just text ->
            case Date.fromString text of
                Result.Ok date ->
                    succeed (Just date)
                Result.Err error ->
                    fail error
                        
        Nothing ->
            succeed Nothing

-- It's OK for "prop6" to be absent
-- but if it exists, it better be a valid date
decoderProp06 : Decoder ( Maybe Date.Date )        
decoderProp06 =
    (maybe (field "prop06" string))
        |> andThen maybeStringDateDecoder
        
-- Ignore that the source actually
-- has a nested object - instead pluck
-- the one value of interest and
-- make that the target property's
-- value.
decoderProp08 : Decoder Int
decoderProp08 =
    at ["prop08","important"] int
--    (field "prop08" (field "important" int))
-- or
--    (field "prop08") <| (field "important" int)
-- are equivalent

decoderNested : Decoder Nested
decoderNested =
     Json.Decode.map3
         Nested
         (field "propN1" string)
         (field "propN2" int)
         (field "propN3" float)

-- The nested object in the source
-- is mapped into a nested record on
-- the target.
decoderProp09 : Decoder Nested
decoderProp09 =
    (field "prop09" decoderNested)

-- Decode prop10, 11, and 12 from the source top level
-- but on the target type shove them into
-- their own nested record instead.
decoderProp101112 : Decoder Nested
decoderProp101112 =
    Json.Decode.map3
        Nested
        (field "prop10" string)
        (field "prop11" int)
        (field "prop12" float)
        
-- decode to a "Subject" with more than 8 fields
decoderSubject : Decoder Subject
decoderSubject =
    let
        stage1 =
            Json.Decode.map8
                Subject
                (field "prop01" string)
                (field "prop02" int)
                (field "prop03" float)
                (field "prop04" bool)
                (maybe (field "prop05" int))
                decoderProp06
                (field "prop07" (list string))
                decoderProp08
    in
        Json.Decode.map3
            (<|)
            stage1
            decoderProp09
            decoderProp101112    


toText : Result String a -> String
toText result =
    case result of
        Result.Ok value ->
            toString value
        Result.Err error ->
            "Error: " ++ error
{-
   Run "decoderSubject" on the "jsonSubject" JSON string and show the result 
   in the browser page. 
-}
main : Html a
main =
    view (toText (Json.Decode.decodeString decoderSubject jsonSubject))
--    view jsonSubject
-- to see the original JSON string instead
rtfeldman (60) [Avatar] Offline
#2
Yep, this is all true!

(And it's exactly why it was possible to implement elm-decode-pipeline to work on decoders of arbitrary length.)