【问题标题】:How to deserialize JSON into flat, Map-like structure?如何将 JSON 反序列化为扁平的、类似 Map 的结构?
【发布时间】:2013-12-19 18:03:59
【问题描述】:

请记住,JSON 结构事先是未知的,即它是完全任意的,我们只知道它是 JSON 格式。

例如,

以下 JSON

{
   "Port":
   {
       "@alias": "defaultHttp",
       "Enabled": "true",
       "Number": "10092",
       "Protocol": "http",
       "KeepAliveTimeout": "20000",
       "ThreadPool":
       {
           "@enabled": "false",
           "Max": "150",
           "ThreadPriority": "5"
       },
       "ExtendedProperties":
       {
           "Property":
           [                         
               {
                   "@name": "connectionTimeout",
                   "$": "20000"
               }
           ]
       }
   }
}

应该反序列化为类似 Map 的结构,其键如下(为简洁起见,并非所有上述内容都包括在内):

port[0].alias
port[0].enabled
port[0].extendedProperties.connectionTimeout
port[0].threadPool.max

我目前正在调查杰克逊,所以我们有:

TypeReference<HashMap<String, Object>> typeRef = new TypeReference<HashMap<String, Object>>() {};
Map<String, String> o = objectMapper.readValue(jsonString, typeRef);

但是,生成的 Map 实例基本上是嵌套 Map 的 Map:

{Port={@alias=diagnostics, Enabled=false, Type=DIAGNOSTIC, Number=10033, Protocol=JDWP, ExtendedProperties={Property={@name=suspend, $=n}}}}

虽然我需要使用“点符号”的扁平键的扁平地图,就像上面一样。

我宁愿自己不实现这个,虽然目前我没有看到任何其他方式......

【问题讨论】:

  • Jackson(或任何其他 JSON 库)可以将 JSON 转换为地图地图。加倍努力并非易事,您展示的示例语法永远不会在运行时在 java 中生成。您可以使用Typesafe Config library 实现与您需要的类似的目标。
  • 好的,所以我做了 Config parseString = ConfigFactory.parseString(portJsonString);其中 toString() 类似于: Config(SimpleConfigObject({"Port":{"Enabled":"false","Number":"10033","Type":"DIAGNOSTIC","@alias":"diagnostics" ,"ExtendedProperties":{"Property":{"@name":"suspend","$":"n"}},"Protocol":"JDWP"}})) 但我不知道如何展平这个通过 Typesafe Config 库?
  • 添加了答案。希望对您有所帮助。

标签: java json jackson flatten


【解决方案1】:

我还必须在我的项目中解决类似的问题,并发现 springframework.vault 有一个方法 flatten() 可以做同样的事情。下面是一个示例代码。


    //Json string to Map<String, Object>

    String data = "Your json as string"
    final ObjectMapper mapper = new ObjectMapper();
    final MapType type = mapper.getTypeFactory().constructMapType(
                Map.class, String.class, Object.class);
    final Map<String, Object> map = mapper.readValue(data, type);

    //Using springframework.vault flatten method

    Map<String, String> keyMap = JsonMapFlattener.flattenToStringMap(map);

    //Input

    {"key": {"nested": 1}, "another.key": ["one", "two"] }

    //Output

      key.nested=1
      another.key[0]=one
      another.key[1]=two

记得添加依赖

    <dependency>
        <groupId>org.springframework.vault</groupId>
        <artifactId>spring-vault-core</artifactId>
        <version>2.1.1.RELEASE</version>
    </dependency>

更多信息,请参考https://docs.spring.io/spring-vault/docs/current/api/org/springframework/vault/support/JsonMapFlattener.html

【讨论】:

    【解决方案2】:

    来自Spring Integrationorg.springframework.integration.transformer.ObjectToMapTransformer 产生所需的结果。 默认情况下,它的 shouldFlattenKeys 属性设置为 true 并生成平面地图(无嵌套,值始终为简单类型)。当shouldFlattenKeys=false 生成嵌套地图

    ObjectToMapTransformer 旨在用作集成流程的一部分,但以独立方式使用它是完全可以的。您需要使用转换输入的有效负载构造org.springframework.messaging.Messagetransform 方法返回 org.springframework.messaging.Message 对象,其有效负载为 Map

    import org.springframework.integration.transformer.ObjectToMapTransformer;
    import org.springframework.messaging.Message;
    import org.springframework.messaging.support.GenericMessage;
    
    Message message = new GenericMessage(value);
     ObjectToMapTransformer transformer = new ObjectToMapTransformer();
            transformer.setShouldFlattenKeys(true);
            Map<String,Object> payload = (Map<String, Object>) transformer
                    .transform(message)
                    .getPayload();
    

    旁注:将 Spring Integration 添加到类路径中只是为了使用单个类可能有点矫枉过正,但您可以检查此类的实现并自己编写类似的解决方案。嵌套映射由 Jackson (org.springframework.integration.support.json.JsonObjectMapper#fromJson(payload, Map.class)) 生成,然后递归遍历 map,将所有作为集合的值展平。

    【讨论】:

    • 最后一行可以简单得多:Map payload = transformer.doTransform(message);
    【解决方案3】:

    您可以这样做来遍历树并跟踪您找出点符号属性名称的深度:

    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.node.ArrayNode;
    import com.fasterxml.jackson.databind.node.ObjectNode;
    import com.fasterxml.jackson.databind.node.ValueNode;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map;
    import org.junit.Test;
    
    public class FlattenJson {
      String json = "{\n" +
          "   \"Port\":\n" +
          "   {\n" +
          "       \"@alias\": \"defaultHttp\",\n" +
          "       \"Enabled\": \"true\",\n" +
          "       \"Number\": \"10092\",\n" +
          "       \"Protocol\": \"http\",\n" +
          "       \"KeepAliveTimeout\": \"20000\",\n" +
          "       \"ThreadPool\":\n" +
          "       {\n" +
          "           \"@enabled\": \"false\",\n" +
          "           \"Max\": \"150\",\n" +
          "           \"ThreadPriority\": \"5\"\n" +
          "       },\n" +
          "       \"ExtendedProperties\":\n" +
          "       {\n" +
          "           \"Property\":\n" +
          "           [                         \n" +
          "               {\n" +
          "                   \"@name\": \"connectionTimeout\",\n" +
          "                   \"$\": \"20000\"\n" +
          "               }\n" +
          "           ]\n" +
          "       }\n" +
          "   }\n" +
          "}";
    
      @Test
      public void testCreatingKeyValues() {
        Map<String, String> map = new HashMap<String, String>();
        try {
          addKeys("", new ObjectMapper().readTree(json), map);
        } catch (IOException e) {
          e.printStackTrace();
        }
        System.out.println(map);
      }
    
      private void addKeys(String currentPath, JsonNode jsonNode, Map<String, String> map) {
        if (jsonNode.isObject()) {
          ObjectNode objectNode = (ObjectNode) jsonNode;
          Iterator<Map.Entry<String, JsonNode>> iter = objectNode.fields();
          String pathPrefix = currentPath.isEmpty() ? "" : currentPath + ".";
    
          while (iter.hasNext()) {
            Map.Entry<String, JsonNode> entry = iter.next();
            addKeys(pathPrefix + entry.getKey(), entry.getValue(), map);
          }
        } else if (jsonNode.isArray()) {
          ArrayNode arrayNode = (ArrayNode) jsonNode;
          for (int i = 0; i < arrayNode.size(); i++) {
            addKeys(currentPath + "[" + i + "]", arrayNode.get(i), map);
          }
        } else if (jsonNode.isValueNode()) {
          ValueNode valueNode = (ValueNode) jsonNode;
          map.put(currentPath, valueNode.asText());
        }
      }
    }
    

    它会生成以下地图:

    Port.ThreadPool.Max=150, 
    Port.ThreadPool.@enabled=false, 
    Port.Number=10092, 
    Port.ExtendedProperties.Property[0].@name=connectionTimeout, 
    Port.ThreadPool.ThreadPriority=5, 
    Port.Protocol=http, 
    Port.KeepAliveTimeout=20000, 
    Port.ExtendedProperties.Property[0].$=20000, 
    Port.@alias=defaultHttp, 
    Port.Enabled=true
    

    删除属性名称中的@$ 应该很容易,尽管由于您说JSON 是任意的,因此您最终可能会在键名中发生冲突。

    【讨论】:

    • 一天结束时,我使用了一个与此非常相似的自定义解决方案,因此将其标记为已接受的答案。谢谢!
    【解决方案4】:

    如何使用 json-flattener。 https://github.com/wnameless/json-flattener

    顺便说一句,我是这个库的作者。

    String flattenedJson = JsonFlattener.flatten(yourJson);
    Map<String, Object> flattenedJsonMap = JsonFlattener.flattenAsMap(yourJson);
    
    // Result:
    {
        "Port.@alias":"defaultHttp",
        "Port.Enabled":"true",
        "Port.Number":"10092",
        "Port.Protocol":"http",
        "Port.KeepAliveTimeout":"20000",
        "Port.ThreadPool.@enabled":"false",
        "Port.ThreadPool.Max":"150",
        "Port.ThreadPool.ThreadPriority":"5",
        "Port.ExtendedProperties.Property[0].@name":"connectionTimeout",
        "Port.ExtendedProperties.Property[0].$":"20000"
    }
    

    【讨论】:

    • 真的很好。希望你继续支持它并准备好生产。如果您还可以在您的 github 存储库上发布性能和测试覆盖率指标,那就太好了。你还在积极开发它吗?
    • 我可以将 map 解压成 json 吗?
    • 效果相对较好,但是这会将我的 Integer 道具转换为 BigDecimal,这对我来说非常丑陋且无用,因为它会强制其他例程从 BigDecimal 重新转换类型。
    【解决方案5】:

    怎么样:

    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Map.Entry;
    import com.google.gson.Gson;
    
    /**
     * NOT FOR CONCURENT USE
    */
    @SuppressWarnings("unchecked")
    public class JsonParser{
    
    Gson gson=new Gson();
    Map<String, String> flatmap = new HashMap<String, String>();
    
    public Map<String, String> parse(String value) {        
        iterableCrawl("", null, (gson.fromJson(value, flatmap.getClass())).entrySet());     
        return flatmap; 
    }
    
    private <T> void iterableCrawl(String prefix, String suffix, Iterable<T> iterable) {
        int key = 0;
        for (T t : iterable) {
            if (suffix!=null)
                crawl(t, prefix+(key++)+suffix);
            else
                crawl(((Entry<String, Object>) t).getValue(), prefix+((Entry<String, Object>) t).getKey());
        }
    }
    
    private void crawl(Object object, String key) {
        if (object instanceof ArrayList)
            iterableCrawl(key+"[", "]", (ArrayList<Object>)object);
        else if (object instanceof Map)
            iterableCrawl(key+".", null, ((Map<String, Object>)object).entrySet());
        else
            flatmap.put(key, object.toString());
    }
    }
    

    【讨论】:

      【解决方案6】:

      您可以使用Typesafe Config Library 实现类似的功能,如下例所示:

      import com.typesafe.config.*;
      import java.util.Map;
      public class TypesafeConfigExample {
        public static void main(String[] args) {
          Config cfg = ConfigFactory.parseString(
            "   \"Port\":\n" +
            "   {\n" +
            "       \"@alias\": \"defaultHttp\",\n" +
            "       \"Enabled\": \"true\",\n" +
            "       \"Number\": \"10092\",\n" +
            "       \"Protocol\": \"http\",\n" +
            "       \"KeepAliveTimeout\": \"20000\",\n" +
            "       \"ThreadPool\":\n" +
            "       {\n" +
            "           \"@enabled\": \"false\",\n" +
            "           \"Max\": \"150\",\n" +
            "           \"ThreadPriority\": \"5\"\n" +
            "       },\n" +
            "       \"ExtendedProperties\":\n" +
            "       {\n" +
            "           \"Property\":\n" +
            "           [                         \n" +
            "               {\n" +
            "                   \"@name\": \"connectionTimeout\",\n" +
            "                   \"$\": \"20000\"\n" +
            "               }\n" +
            "           ]\n" +
            "       }\n" +
            "   }\n" +
            "}");
      
          // each key has a similar form to what you need
          for (Map.Entry<String, ConfigValue> e : cfg.entrySet()) {
            System.out.println(e);
          }
        }
      }
      

      【讨论】:

      • 谢谢你,乔瓦尼。虽然这确实离我需要的更近了一步,但这还不够。例如,它会产生如下内容: Port."@alias"=ConfigString("defaultHttp") 和 Port.ExtendedProperties.Property=SimpleConfigList([{"@name":"jaasRealm","$":"PlatformManagement"}]) - 显然不是扁平的属性集。所以,如果我想使用它,我需要额外解析 Typesafe 库的输出......我想我会使用我自己的解决方案,完成后会在这里发布。
      • 好吧,您要做的是非常“自定义”的事情,因为您有表示不同内容的注释样式字段名称。据我所知,这个概念不是“标准” JSON 或其他,因此您需要在这里做一些工作。我希望 Typesafe 库有所帮助。
      • 如果你想要不同的格式,你只需要做更多的处理,而不是仅仅调用toString。每个条目都有一个路径表达式,如果需要,可以使用ConfigUtil.splitPath 分解,并且可以使用value.unwrapped() 将 ConfigValue 转换为纯 Java 值。给定拆分路径,您可以使用所需的语法重新组合它。
      • @Svilen 你解决了吗?你能发布你的解决方案吗,因为我现在有同样的问题?
      • 我最终使用 Jackson API 自己实现了它。基本上使用杰克逊将 json 解析为地图(地图和列表),然后递归地遍历它并构建平面地图。类似于 siledh 答案
      【解决方案7】:

      如果您事先知道结构,您可以定义一个 Java 类并使用 gson 将 JSON 解析为该类的实例:

      YourClass obj = gson.fromJson(json, YourClass.class); 
      

      如果不是,那么我不确定您要做什么。您显然不能即时定义一个类,因此使用点符号访问已解析的 JSON 是不可能的。

      除非你想要这样的东西:

      Map<String, String> parsed = magicParse(json);
      parsed["Port.ThreadPool.max"]; // returns 150
      

      如果是这样,那么遍历您的地图并构建“扁平化”地图似乎不是什么大问题。

      还是别的什么?

      【讨论】:

      • 型号事先未知。这个想法是将这个扁平化的 JSON 传递给一个比较服务,它可以将它与另一个以相同方式扁平化的 json 进行比较。问题是该服务需要特定的格式,即我试图实现的这种扁平化的东西......我在想这应该不是不常见的问题,所以一些库应该已经可以使用解决方案。
      • @Svilen 为什么不能自己比较 JSON 字符串?
      • siledh,原因是我希望有一种简单的方法可以在创建差异后仅更改单个属性/叶值。这会让我更难做一个纯文本差异......
      • @Svilen 那你为什么需要把它弄平呢? Map 对我来说似乎足够了。
      猜你喜欢
      • 1970-01-01
      • 2011-09-21
      • 2022-09-26
      • 1970-01-01
      • 2011-08-20
      • 1970-01-01
      • 2021-11-19
      • 2011-11-15
      • 2016-05-17
      相关资源
      最近更新 更多