joaonrb
joaonrb

Reputation: 1021

Elm Json.Decode.Pipeline fails decoding existing field

I have an elm decoder and a test for it. The test is passing, but when I use the decoder on the Application, I get a strange error that does not seem consistent with the data passed. I cannot figure out what is wrong. For disclosure, I am new to Elm and this is my first application with it, that is targeted to production.


The decoder

type alias ProviderData =
    { uid : String
    , displayName : Maybe String
    , photoURL : Maybe String
    , email : String
    , phoneNumber : Maybe String
    , providerId : String
    }

type alias TokenManager =
    { apiKey : String
    , refreshToken : String
    , accessToken : String
    , expirationTime : Time.Posix
    }

type alias User =
    { uid : String
    , displayName : Maybe String
    , photoURL : Maybe String
    , email : String
    , emailVerified : Bool
    , phoneNumber : Maybe String
    , isAnonymous : Bool
    , tenantId : Maybe String
    , providerData : List ProviderData
    , apiKey : String
    , appName : String
    , authDomain : String
    , stsTokenManager : TokenManager
    , redirectEventId : Maybe String
    , lastLoginAt : Time.Posix
    , createdAt : Time.Posix
    }

type alias Model =
    { isUserSignedIn : Bool
    , isWaitingForSignInLink : Bool
    , user : Maybe User
    }

unauthenticatedUser : Model
unauthenticatedUser =
    { isUserSignedIn = False
    , isWaitingForSignInLink = False
    , user = Nothing
    }

decoder : Json.Value -> Model
decoder json =
    case Json.decodeValue authDecoder (Debug.log ("Decoding " ++ Json.Encode.encode 4 json) json) of
        Ok model ->
            model

        Err err ->
            Debug.log (Debug.toString err) unauthenticatedUser

authDecoder : Json.Decoder Model
authDecoder =
    Json.succeed Model
        |> required "isUserSignedIn" Json.bool
        |> required "isWaitingForSignInLink" Json.bool
        |> optional "user" (Json.map Just decodeUser) Nothing

decodeUser : Json.Decoder User
decodeUser =
    Json.succeed User
        |> required "uid" Json.string
        |> optional "displayName" (Json.map Just Json.string) Nothing
        |> optional "photoURL" (Json.map Just Json.string) Nothing
        |> required "email" Json.string
        |> required "emailVerified" Json.bool
        |> optional "phoneNumber" (Json.map Just Json.string) Nothing
        |> required "isAnonymous" Json.bool
        |> optional "tenantId" (Json.map Just Json.string) Nothing
        |> required "providerData" (Json.list decodeProviderData)
        |> required "apiKey" Json.string
        |> required "appName" Json.string
        |> required "authDomain" Json.string
        |> required "stsTokenManager" decodeTokenManager
        |> optional "redirectEventId" (Json.map Just Json.string) Nothing
        |> required "lastLoginAt" timestampFromString
        |> required "createdAt" timestampFromString

decodeProviderData : Json.Decoder ProviderData
decodeProviderData =
    Json.succeed ProviderData
        |> required "uid" Json.string
        |> optional "displayName" (Json.map Just Json.string) Nothing
        |> optional "photoURL" (Json.map Just Json.string) Nothing
        |> required "email" Json.string
        |> optional "phoneNumber" (Json.map Just Json.string) Nothing
        |> required "providerId" Json.string

decodeTokenManager : Json.Decoder TokenManager
decodeTokenManager =
    Json.succeed TokenManager
        |> required "apiKey" Json.string
        |> required "refreshToken" Json.string
        |> required "accessToken" Json.string
        |> required "expirationTime" timestampFromInt

timestampFromString : Json.Decoder Time.Posix
timestampFromString =
    Json.andThen
        (\str ->
            case String.toInt str of
                Just ts ->
                    Json.succeed (Time.millisToPosix ts)

                Nothing ->
                    Json.fail (str ++ " is not a timetamp")
        )
        Json.string

timestampFromInt : Json.Decoder Time.Posix
timestampFromInt =
    Json.andThen
        (\ts ->
            Json.succeed (Time.millisToPosix ts)
        )
        Json.int

unauthenticatedUser : Model
unauthenticatedUser =
    { isUserSignedIn = False
    , isWaitingForSignInLink = False
    , user = Nothing
    }

The Error

Failure "Json.Decode.oneOf failed in the following 2 ways:



(1) Problem with the given value:
    
    {
            "uid": "",
            "displayName": null,
            "photoURL": null,
            "email": "[email protected]",
            "emailVerified": true,
            "phoneNumber": null,
            "isAnonymous": false,
            "tenantId": null,
            "providerData": [
                {
                    "uid": "[email protected]",
                    "displayName": null,
                    "photoURL": null,
                    "email": "[email protected]",
                    "phoneNumber": null,
                    "providerId": "password"
                }
            ],
            "apiKey": "",
            "appName": "[DEFAULT]",
            "authDomain": "example.firebaseapp.com",
            "stsTokenManager": {
                "apiKey": "",
                "refreshToken": "",
                "accessToken": "",
                "expirationTime": 1603998440000
            },
            "redirectEventId": null,
            "lastLoginAt": "1603576515267",
            "createdAt": "1603573117442",
            "multiFactor": {
                "enrolledFactors": []
            }
        }
    
    Expecting an OBJECT with a field named `createdAt`



(2) Problem with the given value:
    
    {
            "uid": "",
            "displayName": null,
            "photoURL": null,
            "email": "[email protected]",
            "emailVerified": true,
            "phoneNumber": null,
            "isAnonymous": false,
            "tenantId": null,
            "providerData": [
                {
                    "uid": "[email protected]",
                    "displayName": null,
                    "photoURL": null,
                    "email": "[email protected]",
                    "phoneNumber": null,
                    "providerId": "password"
                }
            ],
            "apiKey": "",
            "appName": "[DEFAULT]",
            "authDomain": "example.firebaseapp.com",
            "stsTokenManager": {
                "apiKey": "",
                "refreshToken": "",
                "accessToken": "",
                "expirationTime": 1603998440000
            },
            "redirectEventId": null,
            "lastLoginAt": "1603576515267",
            "createdAt": "1603573117442",
            "multiFactor": {
                "enrolledFactors": []
            }
        }
    
    Expecting null" <internals>: { isUserSignedIn = False, isWaitingForSignInLink = False, user = Nothing }

The Test

module Tests exposing (..)

import Expect
import Json.Decode as Json
import Modules.Firebase as Firebase
import Test exposing (..)
import Time


all : Test
all =
    describe "Test Firebase"
        [ test "Test auth JSON" <|
            \_ ->
                let
                    input =
                        """
                          { 
                              "isUserSignedIn": true,
                              "isWaitingForSignInLink": false,
                              "user": {
                                "uid": "xxxxxxxxxxxx",
                                "displayName": null,
                                "photoURL": null,
                                "email": "[email protected]",
                                "emailVerified": true,
                                "phoneNumber": null,
                                "isAnonymous": false,
                                "tenantId": null,
                                "providerData": [
                                  {
                                    "uid": "[email protected]",
                                    "displayName": null,
                                    "photoURL": null,
                                    "email": "[email protected]",
                                    "phoneNumber": null,
                                    "providerId": "password"
                                  }
                                ],
                                "apiKey": "apikey.xxxxxxxxx",
                                "appName": "[DEFAULT]",
                                "authDomain": "example.com",
                                "stsTokenManager": {
                                  "apiKey": "apikey.xxxxxxxxx",
                                  "refreshToken": "refresh.xxxxxxxxxxxx",
                                  "accessToken": "access.xxxxxxxxxxxx",
                                  "expirationTime": 1603825391000
                                },
                                "redirectEventId": null,
                                "lastLoginAt": "1603576515267",
                                "createdAt": "1603573117442",
                                "multiFactor": {
                                  "enrolledFactors": []
                                }
                              }
                          }
                        """

                    decodedOutput =
                        case Json.decodeString Json.value input of
                            Ok value ->
                                Ok (Firebase.decoder value)

                            Err err ->
                                Err err
                in
                Expect.equal decodedOutput
                    (Ok firebaseModel)
        ]

firebaseModel : Firebase.Model
firebaseModel =
    { isUserSignedIn = True
    , isWaitingForSignInLink = False
    , user =
        Just
            { uid = "xxxxxxxxxxxx"
            , displayName = Nothing
            , photoURL = Nothing
            , email = "[email protected]"
            , emailVerified = True
            , phoneNumber = Nothing
            , isAnonymous = False
            , tenantId = Nothing
            , providerData =
                [ { uid = "[email protected]"
                  , displayName = Nothing
                  , photoURL = Nothing
                  , email = "[email protected]"
                  , phoneNumber = Nothing
                  , providerId = "password"
                  }
                ]
            , apiKey = "apikey.xxxxxxxxx"
            , appName = "[DEFAULT]"
            , authDomain = "example.com"
            , stsTokenManager =
                { apiKey = "apikey.xxxxxxxxx"
                , refreshToken = "refresh.xxxxxxxxxxxx"
                , accessToken = "access.xxxxxxxxxxxx"
                , expirationTime = Time.millisToPosix 1603825391000
                }
            , redirectEventId = Nothing
            , lastLoginAt = Time.millisToPosix 1603576515267
            , createdAt = Time.millisToPosix 1603573117442
            }
    }

Edited at 2020-10-30

Debug Info

Trying to debug this issue I started to incrementally build the model, field by field, to understand what was wrong. The following model and decoder work fine:

... 
type alias User =
    { uid : String
    , displayName : Maybe String
    , photoURL : Maybe String
    , email : String
    , emailVerified : Bool
    , phoneNumber : Maybe String
    , isAnonymous : Bool
    , tenantId : Maybe String
    , providerData : List ProviderData
    }
...
decodeUser : Json.Decoder User
decodeUser =
    Json.succeed User
        |> required "uid" Json.string
        |> optional "displayName" (Json.nullable Json.string) Nothing
        |> optional "photoURL" (Json.nullable Json.string) Nothing
        |> required "email" Json.string
        |> required "emailVerified" Json.bool
        |> optional "phoneNumber" (Json.nullable Json.string) Nothing
        |> required "isAnonymous" Json.bool
        |> optional "tenantId" (Json.nullable Json.string) Nothing
        |> required "providerData" (Json.list decodeProviderData)
...

Note 1- All the other code remains the same.

Note 2- The input data remains the same.

Note 3 - I changed Json.map Just to Json.nullable by @5ndG suggestion. I also tried optional "field" (Json.nullable Json.string) Nothing to required "field" (Json.nullable Json.string) but the issue persist.

With this change, the model gets to the expected state, minus the fields I removed. By adding the next field, |> required "apiKey" Json.string, it starts failing with the same issue.

Upvotes: 1

Views: 611

Answers (2)

joaonrb
joaonrb

Reputation: 1021

I fixed this issue by making the first parameter a string and force my service to pass a JSON string instead of a Json.Decode.Value.

I change the decorator to:


decoder : String -> Model
decoder json =
    case Json.decodeString Json.value json of
        Ok value ->
            decodeValue value

        Err _ ->
            unauthenticatedUser


decodeValue : Json.Value -> Model
decodeValue json =
    case Json.decodeValue authDecoder json of
        Ok model ->
            model

        Err _ ->
            Debug.log unauthenticatedUser

Not sure why the can't I use the Json.Decode.Value from the service, but this solves the problem.

Upvotes: 2

8n8
8n8

Reputation: 1382

Edit: this answer is wrong, so ignore it.

I can reproduce your error with this input in the test:

{ 
    "isUserSignedIn": true,
    "isWaitingForSignInLink": false
}

That is, miss out the user field entirely.

From reading the docs, I think this is a bug in the optional function.

As described here, you can fix this by replacing the user decoder with:

       |> optional "user" (Json.nullable decodeUser) Nothing

Upvotes: 0

Related Questions