【问题标题】:Decoding a Java/JSON Map into an F# object将 Java/JSON 映射解码为 F# 对象
【发布时间】:2026-01-21 22:35:02
【问题描述】:

我在将 Java/JSON 映射转换为可用的 F# 对象时遇到问题。

这是我的代码的核心:

member this.getMapFromRpcAsynchronously =
    Rpc.getJavaJSONMap (new Action<_>(this.fillObjectWithJSONMap))
    ()

member this.fillObjectWithJSONMap (returnedMap : JSONMap<string, int> option) = 
    let container = Option.get(returnedMap)
    let map = container.map
    for thing in map do
        this.myObject.add thing.key
        // do stuff with thing
    ()

我的 RPC 方法返回的 JSON 如下所示:

{"id":1, "result":
    {"map":
        {"Momentum":12, "Corporate":3, "Catalyst":1},
     "javaClass":"java.util.HashMap"}
}

我正在尝试将其映射到如下所示的 F# DataContract:

[<DataContract>]
type JSONMap<'T, 'S> = {
    [<DataMember>]
    mutable map : KeyValuePair<'T, 'S> array
    [<DataMember>]
    mutable javaClass : string
}

[<DataContract>]
type JSONSingleResult<'T> = {
    [<DataMember>]
    mutable javaClass: string
    [<DataMember>]
    mutable result: 'T
}

最后,执行实际 RPC 调用的 F# 方法(上面的 Rpc.getJavaJSONMap)如下所示:

let getJavaJSONMap (callbackUI : Action<_>) = 
    ClientRpc.getSingleRPCResult<JSONSingleResult<JSONMap<string, int>>, JSONMap<string, int>>
        "MyJavaRpcClass"
        "myJavaRpcMethod"
        "" // takes no parameters
        callbackUI
        (fun (x : option<JSONSingleResult<JSONMap<string, int>>>) ->
            match x.IsSome with
                | true -> Some(Option.get(x).result)
                | false -> None 
        )

在编译时我没有收到任何错误。我的 RPC 方法被调用,并返回一个结果(使用 Fiddler 查看实际调用和返回)。但是,F# 似乎无法将 JSON 匹配到我的 DataContract 中,因为最顶部的 returnedMap 始终为空。

任何想法或建议将不胜感激。谢谢。

【问题讨论】:

    标签: java json f# map datacontract


    【解决方案1】:

    这是我做的:

    open System.Web.Script.Serialization   // from System.Web.Extensions assembly
    
    let s = @"
        {""id"":1, ""result"": 
            {""map"": 
                {""Momentum"":12, ""Corporate"":3, ""Catalyst"":1}, 
             ""javaClass"":""java.util.HashMap""} 
        } 
        "
    
    let jss = new JavaScriptSerializer()
    let o = jss.DeserializeObject(s)
    
    // DeserializeObject returns nested Dictionary<string,obj> objects, typed
    // as 'obj'... so add a helper dynamic-question-mark operator
    open System.Collections.Generic 
    let (?) (o:obj) name : 'a = (o :?> Dictionary<string,obj>).[name] :?> 'a
    
    printfn "id: %d" o?id
    printfn "map: %A" (o?result?map 
                       |> Seq.map (fun (KeyValue(k:string,v)) -> k,v) 
                       |> Seq.toList)
    // prints:
    // id: 1
    // map: [("Momentum", 12); ("Corporate", 3); ("Catalyst", 1)]
    

    【讨论】:

      【解决方案2】:

      嗯,这是一个复杂的问题。我假设:

      {"map": 
              {"Momentum":12, "Corporate":3, "Catalyst":1}, 
           "javaClass":"java.util.HashMap"} 
      

      可以包含可变数量的字段。并且在 JSON 表示法中转换为一个对象(javascript 对象基本上(或非常相似)地图)。我不知道这是否会直接转换为 F#。

      F# 静态类型与 javascript 的动态类型相比可能会被阻止。

      您可能必须自己编写转换例程。


      好的,数据合约中有几个小错误,让我们重新定义 JsonMap 并删除“javaclass”属性,因为它不在提供的 JSON 样本中(它是更高级别的),看起来好像我的 keyvaulepair 没有序列化,所以让我们定义我们自己的类型:

      type JsonKeyValuePair<'T, 'S> =  {
          [<DataMember>] 
          mutable key : 'T
          [<DataMember>] 
          mutable value : 'S
      }
      
      type JSONMap<'T, 'S> = { 
          [<DataMember>] 
          mutable map : JsonKeyValuePair<'T, 'S> array 
      } 
      

      并创建一个反序列化函数:

      let internal deserializeString<'T> (json: string)  : 'T = 
          let deserializer (stream : MemoryStream) = 
              let jsonSerializer 
                  = Json.DataContractJsonSerializer(
                      typeof<'T>)
              let result = jsonSerializer.ReadObject(stream)
              result
      
      
          let convertStringToMemoryStream (dec : string) : MemoryStream =
              let data = Encoding.Unicode.GetBytes(dec); 
              let stream = new MemoryStream() 
              stream.Write(data, 0, data.Length); 
              stream.Position <- 0L
              stream
      
          let responseObj = 
              json
                  |> convertStringToMemoryStream
                  |> deserializer
      
          responseObj :?> 'T
      
      
      let run2 () = 
          let json = "{\"map@\":[{\"key@\":\"a\",\"value@\":1},{\"key@\":\"b\",\"value@\":2}]}"
          let o  = deserializeString<JSONMap<string, int>> json
          ()
      

      我能够将字符串反序列化为适当的对象结构。我希望看到的两件事是

      1) 为什么 .NET 强制我在字段名称后附加 @ 字符? 2)进行转换的最佳方法是什么?我猜想代表 JSON 结构的抽象语法树可能是要走的路,然后将其解析为新字符串。不过,我对 AST 及其解析不是很熟悉。

      也许其中一位 F# 专家可以提供帮助或提出更好的翻译方案?


      最后加回结果类型:

      [<DataContract>] 
      type Result<'T> = { 
          [<DataMember>] 
          mutable javaClass: string 
          [<DataMember>] 
          mutable result: 'T 
      } 
      

      和一个转换映射函数(在这种情况下有效 - 但有许多弱点,包括递归映射定义等):

      let convertMap (json: string) = 
          let mapToken = "\"map\":"
          let mapTokenStart = json.IndexOf(mapToken)
          let mapTokenStart  = json.IndexOf("{", mapTokenStart)
          let mapObjectEnd  = json.IndexOf("}", mapTokenStart)
          let mapObjectStart = mapTokenStart
          let mapJsonOuter = json.Substring(mapObjectStart, mapObjectEnd - mapObjectStart + 1)
          let mapJsonInner = json.Substring(mapObjectStart + 1, mapObjectEnd - mapObjectStart - 1)
          let pieces = mapJsonInner.Split(',')
          let convertPiece state (piece: string) = 
              let keyValue = piece.Split(':')
              let key = keyValue.[0]
              let value = keyValue.[1]
              let newPiece = "{\"key\":" + key + ",\"value\":" + value + "}"
              newPiece :: state
      
          let newPieces = Array.fold convertPiece []  pieces
          let newPiecesArr = List.toArray newPieces
          let newMap = String.Join(",",  newPiecesArr)
          let json = json.Replace(mapJsonOuter, "[" + newMap + "]")
          json
      
      
      
      let json = "{\"id\":1, \"result\": {\"map\": {\"Momentum\":12, \"Corporate\":3, \"Catalyst\":1}, \"javaClass\":\"java.util.HashMap\"} } "
      printfn <| Printf.TextWriterFormat<unit>(json)
      let json2 = convertMap json
      printfn <| Printf.TextWriterFormat<unit>(json2)
      let obj = deserializeString<Result<JSONMap<string,int>>> json2
      

      它仍然在各个地方的 @ 符号上出现 - 我不明白......


      为&符号问题添加带有解决方法的转换

      let convertMapWithAmpersandWorkAround (json: string) = 
          let mapToken = "\"map\":"
          let mapTokenStart = json.IndexOf(mapToken)
          let mapObjectEnd  = json.IndexOf("}", mapTokenStart)
          let mapObjectStart = json.IndexOf("{", mapTokenStart)
          let mapJsonOuter = json.Substring(mapTokenStart , mapObjectEnd - mapTokenStart + 1)
          let mapJsonInner = json.Substring(mapObjectStart + 1, mapObjectEnd - mapObjectStart - 1)
          let pieces = mapJsonInner.Split(',')
          let convertPiece state (piece: string) = 
              let keyValue = piece.Split(':')
              let key = keyValue.[0]
              let value = keyValue.[1]
              let newPiece = "{\"key@\":" + key + ",\"value@\":" + value + "}"
              newPiece :: state
      
          let newPieces = Array.fold convertPiece []  pieces
          let newPiecesArr = List.toArray newPieces
          let newMap = String.Join(",",  newPiecesArr)
          let json = json.Replace(mapJsonOuter, "\"map@\":[" + newMap + "]")
          json
      
      
      
      let json = "{\"id\":1, \"result\": {\"map\": {\"Momentum\":12, \"Corporate\":3, \"Catalyst\":1}, \"javaClass\":\"java.util.HashMap\"} } "
      printfn <| Printf.TextWriterFormat<unit>(json)
      let json2 = convertMapWithAmpersandWorkAround json
      printfn <| Printf.TextWriterFormat<unit>(json2)
      let obj = deserialize<Result<JSONMap<string,int>>> json2
      

      添加:

      [<DataContract>] 
      

      以上记录修复了 & 号问题。

      【讨论】:

      • 我注意到,当我将 F# 记录类型存储到 RavenDB 时,它们将使用常规属性名称存储,并且相同的属性名称后跟一个 @ 符号。我怀疑 @ 版本是记录类型中公共属性的支持字段。创建我自己的类而不是使用记录摆脱了额外的 @ 属性 - 你可能不得不做类似的事情。
      最近更新 更多