Elm Json.Decode.Pipeline 解码现有字段失败

Elm Json.Decode.Pipeline fails decoding existing field

我有一个 elm 解码器和一个测试。测试通过了,但是当我在应用程序上使用解码器时,我得到一个奇怪的错误,似乎与通过的数据不一致。我不知道出了什么问题。作为披露,我是 Elm 的新手,这是我的第一个应用程序,它是针对生产的。


解码器

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
    }

错误

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



(1) Problem with the given value:
    
    {
            "uid": "",
            "displayName": null,
            "photoURL": null,
            "email": "joe@mail.com",
            "emailVerified": true,
            "phoneNumber": null,
            "isAnonymous": false,
            "tenantId": null,
            "providerData": [
                {
                    "uid": "joe@mail.com",
                    "displayName": null,
                    "photoURL": null,
                    "email": "joe@mail.com",
                    "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": "joe@mail.com",
            "emailVerified": true,
            "phoneNumber": null,
            "isAnonymous": false,
            "tenantId": null,
            "providerData": [
                {
                    "uid": "joe@mail.com",
                    "displayName": null,
                    "photoURL": null,
                    "email": "joe@mail.com",
                    "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 }

测试

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": "joe@mail.com",
                                "emailVerified": true,
                                "phoneNumber": null,
                                "isAnonymous": false,
                                "tenantId": null,
                                "providerData": [
                                  {
                                    "uid": "joe@mail.com",
                                    "displayName": null,
                                    "photoURL": null,
                                    "email": "joe@mail.com",
                                    "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 = "joe@mail.com"
            , emailVerified = True
            , phoneNumber = Nothing
            , isAnonymous = False
            , tenantId = Nothing
            , providerData =
                [ { uid = "joe@mail.com"
                  , displayName = Nothing
                  , photoURL = Nothing
                  , email = "joe@mail.com"
                  , 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
            }
    }

编辑于 2020-10-30

调试信息

为了调试这个问题,我开始逐个字段地逐步构建模型,以了解问题所在。以下模型和解码器工作正常:

... 
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)
...

注1-所有其他代码保持不变。

注2- 输入数据保持不变。

注 3 - 我根据@5ndG 的建议将 Json.map Just 更改为 Json.nullable。我也试过 optional "field" (Json.nullable Json.string) Nothingrequired "field" (Json.nullable Json.string) 但问题仍然存在。

通过此更改,模型达到预期状态,减去我删除的字段。通过添加下一个字段 |> required "apiKey" Json.string,它开始因同样的问题而失败。

编辑:此答案错误,请忽略。

我可以在测试中用这个输入重现你的错误:

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

也就是说,完全错过 user 字段。

通过阅读 docs,我认为这是 optional 函数中的错误。

here 所述,您可以通过将 user 解码器替换为以下内容来解决此问题:

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

我通过将第一个参数设为字符串并强制我的服务传递 JSON 字符串而不是 Json.Decode.Value.

来解决此问题

我把装饰器改成:


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

不确定为什么我不能使用服务中的 Json.Decode.Value,但这解决了问题。