Elm - 使用动态键解码Json

3
我希望解码一个类似于以下格式的Json文件:
{ 'result': [
    {'id': 1, 'model': 'online', 'app_label': 'some_app_users'}, 
    {'id': 2, 'model': 'rank', 'app_label': 'some_app_users'}, 
]}

或者像这样:

或者像这样:

{ 'result': [
    {'id': 1, 'name': 'Tom', 'skills': {'key': 'value', ...}, {'key': 'value', ...}},
    {'id': 1, 'name': 'Bob', 'skills': {'key': 'value', ...}, {'key': 'value', ...}},
]}

基本上,result 下的内容是一个具有相同键的字典列表 - 但我事先不知道这些键,也不知道它们的值类型(int、string、dict等)。

目标是显示数据库表格内容;Json 包含 SQL 查询的结果。

我的解码器看起来像这样(未编译):

tableContentDecoder : Decode.Decoder (List dict)
tableContentDecoder =
    Decode.at [ "result" ] (Decode.list Decode.dict)

我这样使用它:
Http.send GotTableContent (Http.get url tableContentDecoder)

我遇到了这个错误:

函数 list 期望的参数是: Decode.Decoder (Dict.Dict String a)

但实际上是: Decode.Decoder a -> Decode.Decoder (Dict.Dict String a)

正确使用字典解码器的语法是什么?这样会起作用吗?我找不到任何通用的 Elm 解码器...
2个回答

4
Decode.list是一个函数,它接受类型为Decoder a的值,并返回类型为Decoder (List a)的值。Decode.dict也是一个函数,它接受类型为Decoder a的值,并返回类型为Decoder (Dict String a)的值。这告诉我们两件事:
  • 在将其传递给Decoder.list之前,我们需要向Decode.dict传递一个解码器。
  • 字典可能不适合您的用例,因为字典只能在两个固定类型之间进行映射,并且不支持像'skills': {'key': 'value', ...}这样的嵌套值。

Elm没有提供通用的解码器。这是由于Elm保证“没有运行时错误”的动机所致。在处理外部世界时,Elm需要保护其运行时免受外部故障、错误等可能性的影响。 Elm实现此目标的主要机制是类型。 Elm只允许正确描述的数据进入,通过这样做消除了通用解码器可能引入的错误的可能性。

由于您的主要目标是显示内容,类似于Dict String String可能有效,但这取决于数据的嵌套深度。您可以通过对代码进行小修改来实现此目的:Decode.at [ "result" ] <| Decode.list (Decode.dict Decode.string)

另一个可能性是使用Decode.valueDecode.andThen来测试指示我们正在读取哪个表的值。

重要的是,我们的解码器具有单一一致的类型,这意味着我们需要将可能的结果表示为总和类型。

-- represents the different possible tables
type TableEntry
    = ModelTableEntry ModelTableFields
    | UserTableEntry  UserTableFields
    | ... 

-- we will use this alias as a constructor with `Decode.map3`
type alias ModelTableFields =
    { id       : Int
    , model    : String
    , appLabel : String
    }

type alias UserTableFields =
    { id : Int
    , ...
    }

tableContentDecoder : Decoder (List TableEntry)
tableContentDecoder =
    Decode.value 
        |> Decode.andThen 
            \value ->
                let
                    tryAt field = 
                        Decode.decodeValue
                            (Decode.at ["result"] <| 
                                Decode.list <|
                                Decode.at [field] Decode.string)
                            value
                in  
                    -- check the results of various attempts and use
                    -- the appropriate decoder based on results
                    case ( tryAt "model", tryAt "name", ... ) of
                        ( Ok _, _, ... ) ->
                            decodeModelTable

                        ( _, Ok _, ... ) ->
                            decodeUserTable

                        ...

                        (_, _, ..., _ ) ->
                            Decode.fail "I don't know what that was!"

-- example decoder for ModelTableEntry
-- Others can be constructed in a similar manner but, you might
-- want to use NoRedInk/Json.Decode.Pipline for more complex data
decodeModel : Decoder (List TableEntry)
decodeModel  =
    Decode.list <|
       Decode.map3 
           (ModelTableEntry << ModelTableFields)
           (Decode.field "id" Decode.int)
           (Decode.field "model" Decode.string)
           (Decode.field "app_label" Decode.string) 

decodeUser : Decoder (List TableEntry)
decodeUser = 
    ...

可以说,与大多数其他语言相比,解析JSON需要做更多的工作。然而,这样做有利于使用外部数据而不必担心异常。

一种思考方式是,Elm让你在前期就做好所有的工作。其他语言可能会让你更快地上手,但是对于帮助你实现稳定性的帮助却较少。


谢谢@Tyler。我理解Elm解码器背后的原理。我已经使用预定义的有效负载OK,但我这里的问题实际上是关于动态有效负载(显示随机表格内容)。我尝试使用Decode.at [ "result" ] <| (Decode.list (Decode.dict Decode.string)),它确实编译通过,并且HTTP请求也很好(在“Ok”分支中执行的代码),但我在模型中得到的结果是一个空列表...我不知道如何调试它。 - François Constant
我找到了一种方法,非常接近你的解决方案。稍后会更新我的问题并提供更多细节。 - François Constant

0

我无法弄清楚如何让Decode.dict工作,所以我改变了我的Json并分割了列和结果:

data={
    'columns': [column.name for column in cursor.description],
    'results': [[str(column) for column in record] for record in cursor.fetchall()]
}

我还必须将所有结果转换为字符串,以使其简单化。例如,Json 将具有 'id': "1"

使用这种方式完成的 Json,Elm 代码非常简单:

type alias QueryResult =
    { columns : List String, results : List (List String) }

tableContentDecoder : Decode.Decoder QueryResult
tableContentDecoder =
    Decode.map2
        QueryResult
        (Decode.field "columns" (Decode.list Decode.string))
        (Decode.field "results" (Decode.list (Decode.list Decode.string)))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接