MackeyRMS/json-decode-attempt/1.0.0

NOTE: the latest version is 3.0.1

Json.Decode.Attempt

When decoding Json, there is usually a hard trade off

  1. Risk failing to decode (bad) in order to get error messages (good)
  2. Have fallbacks (good) but suppress error messages (bad)

This attempt decoding technique is a way to get the good of both. The result of decoding an Attempt gives you the value you sought and a list of errors it accumulated along the way.

Attempt

type Attempt value safety

An attempt at decoding a particular value. The safety parameter describes if this kind of attempt will be able to recover or if it will fail hard.

A marker that this Attempt value Dangerously cannot be safely decoded straight to a value, and it may fail in a big way that doesnt deliver a value at all. In addition, if it does succeed decoding a value, it still preserves the Decode.Error is found along the way.

type alias User =
    { id : Int
    , name : Maybe String
    }


decodeUser : Decode.Value -> Result Decode.Error ( User, List Decode.Error )
decodeUser json =
    let
        attemptUser : Attempt User Dangerously
        attemptUser =
            Attempt.succeed User
                |> Attempt.risk (Decode.field "id" Decode.int)
                |> Attempt.try (Decode.map Just <| Decode.field "name" Decode.string) Nothing
    in
    Attempt.decodeValueDangerously attemptUser json
type Safely

A marker that this Attempt value Safely can be safely decoded straight to a value, without an intermediate Result Decode.Error value. In this example below, attemptInt will always give you an OperatingSystem, and it may or may not give you an error too.

attemptOperatingSystem : Attempt OperatingSystem Safely
attemptOperatingSystem =
    Attempt.fromDecoderWithDefault
        (Decode.field "operating_system" OperatingSystem.decoder)
        OperatingSystem.NotDetected


( value, errors ) =
    Attempt.decodeValueSafely attemptOperatingSystem

Pipeline

try : Decoder a -> a -> Attempt (a -> b) safety -> Attempt b safety

Try a decoder, and if it fails, fallback Safely to a fallback value

type alias Flags =
    { landingPageUrl : String }


attemptFlags : Attempt Flags Safely
attemptFlags =
    Attempt.succeed Flags
        |> Attempt.try Decode.string (Route.toString Route.home)
risk : Decoder a -> Attempt (a -> b) safety -> Attempt b Dangerously

Try a decoder, and risk it failing, thereby transforming your attempt into a Dangerous one

type Id
    = Id Int


attemptId : Attempt Id Dangerous
attemptId =
    Attempt.succeed Id
        |> Attempt.risk Decode.int
applySafely : Attempt a Safely -> Attempt (a -> b) safety -> Attempt b safety

Apply a safe attempt to a pipeline. Kind of like a more general form of try

try : Decoder a -> a -> Attempt (a -> b) safety -> Attempt b safety
try decoder fallback =
    applySafely (fromDecoderWithFallback decoder fallback)


type alias Flags =
    { landingPageUrl : String }


attemptFlags : Attempt Flags Safely
attemptFlags =
    Attempt.succeed Flags
        |> Attempt.applySafely (fromDecoderWithFallback Decode.string (Route.toString Route.home))

Apply a dangerous attempt to a pipeline, turning the rest of the pipeline into a dangerous attempt. Kind of like a more general form of risk.

risk : Decoder a -> Attempt (a -> b) safety -> Attempt b Dangerously
risk decoder =
    applyDangerously (fromDecoder decoder)


type Id
    = Id Int


attemptId : Attempt Id Dangerous
attemptId =
    Attempt.succeed Id
        |> Attempt.risk Decode.int

Basics

Use a decoder in an attempt. Since the decoder might fail, the resulting attempt is a Dangerous one.

fromValue : value -> Attempt value Safely

Convert a value into an attempt, presumably to use it in a pipeline. Since the value is provided outright the Attempt is safe

fromDecoderWithFallback : Decoder a -> a -> Attempt a Safely

Use a decoder in an attempt, with a fallback. Since a fallback is provided, the attempt is safe

fallbackTo : value -> Attempt value Dangerously -> Attempt value Safely

Take a dangerous attempt, and specify how it should fall back, thereby making it a safe attempt

field : String -> Attempt value safety -> Attempt value Dangerously

Attempt to decode json that is under a certain field, much like Decode.field. By checking under a field, you risk the field not being present in the first place, so using field is always Dangerous

type alias User =
    { id : Int
    , name : Maybe String
    , car : Car
    }


type alias Car =
    { plateNumber : Maybe String
    , miles : Maybe Int
    }


attemptCar : Attempt Car Safely
attemptCar =
    Attempt.fromValue Car
        |> Attempt.try (Decode.map Just <| Decode.field "plate_number" Decode.string) Nothing
        |> Attempt.try (Decode.map Just <| Decode.field "miles" Decode.int) Nothing


attemptUser : Attempt User Dangerously
attemptUser =
    Attempt.fromValue User
        |> Attempt.risk (Decode.field "id" Decode.int)
        |> Attempt.try (Decode.map Just <| Decode.field "name" Decode.string) Nothing
        |> Attempt.applyDangerously (Attempt.field "car" attemptCar)
map : (a -> b) -> Attempt a safety -> Attempt b safety

Convert the value of an Attempt from one type to another

Decode

-> String
-> Result Error ( value, List Error )

Just like decodeValueDangerously, except it takes a String instead of a Decode.Value. Just like the difference between Decode.decodeValue and Decode.decodeString.

decodeStringSafely : Attempt value Safely -> String -> ( value, List Error )

Just like decodeValueSafely, except it takes a String instead of a Decode.Value. Just like the difference between Decode.decodeValue and Decode.decodeString.

-> Value
-> Result Error ( value, List Error )

The final step in an attempt to decode json. This function takes a dangerous attempt, and therefore it can possibly fail. If it does succeed, the Ok case is (value, List Decode.Error), containing both the desired value and the List Decode.Error of non-critical errors it encountered along the way.

decodeValueSafely : Attempt value Safely -> Value -> ( value, List Error )

The final step in an attempt to decode json. This function takes a safe attempt, and therefore can never fail. It will pass along both the value you intended to decode, and a list of whatever errors it encountered along the way.