如何将JSON反序列化为扁平的、类似于Map的结构?

26

请注意,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

我目前正在研究Jackson,所以我们有:

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库实现类似于所需内容的东西。 - Giovanni Botta
好的,我执行了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库来展开它? - Svilen
已添加答案。希望能有所帮助。 - Giovanni Botta
7个回答

52
您可以使用此方法遍历树形结构并跟踪深度,以便确定点表示法属性名称:
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是任意的,可能会导致键名冲突。


1
最后我选择了一个非常类似于这个的自定义解决方案,因此将其标记为已接受的答案。谢谢! - Svilen

43

试试使用 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存储库上发布性能和测试覆盖度指标,那就太好了。您还在积极开发吗? - Ashish Thukral
1
我可以将unflatt map转换成JSON吗? - PDS
1
工作相对不错,但是这会将我的整数属性转换为BigDecimal,这非常难看且对我没有用处,因为它会迫使额外的程序来重新转换BigDecimal类型。 - erik.aortiz
这是一个很棒的库。 - Anmol Jain
哪个Maven依赖包具有展开功能? - undefined
显示剩余2条评论

8

那么怎么样:

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());
}
}

4

org.springframework.integration.transformer.ObjectToMapTransformer 来自 Spring Integration,能够产生所需的结果。 默认情况下,它的 shouldFlattenKeys 属性设置为 true 并产生扁平的映射(无嵌套,值始终是简单类型)。当 shouldFlattenKeys=false 时,它会产生嵌套的映射。

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

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)),然后递归遍历映射,将所有集合值展平。


1
最后一行可以简化为: Map payload = transformer.doTransform(message); - Vikas Prasad

3

我在我的项目中也遇到类似的问题,后来发现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


1
您可以使用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库的输出...我想我会使用自己的解决方案,并在完成后在这里发布。 - Svilen
你所尝试做的是非常“定制化”的事情,因为你有注释样式的字段名称代表不同的东西。据我所知,这个概念不是“标准”的JSON或其他格式,因此你需要做一些功课。希望Typesafe库能够帮到你。 - Giovanni Botta
1
如果您想要不同的格式,而不仅仅是调用toString,那么您只需要进行更多的处理。每个条目都有一个路径表达式,如果您愿意,可以使用ConfigUtil.splitPath将其分解,并且可以使用value.unwrapped()将ConfigValue转换为普通Java值。根据拆分的路径,您可以使用所需的语法重新组装它。 - Havoc P
@Svilen,你解决了吗?你能把你的解决方案发出来吗?因为我现在也遇到了同样的问题。 - Battle_Slug
1
最终我使用Jackson API自己实现了它。基本上使用Jackson将JSON解析为一个映射(包含映射和列表),然后递归遍历并构建一个扁平的映射。与siledh的答案类似。 - Svilen

-1

如果您事先了解结构,可以定义一个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
@Svilen 为什么你不能直接比较JSON字符串本身呢? - siledh
siledh,原因是我想在进行差异比较后轻松更改单个属性/叶值。如果只有纯文本差异,这将更加困难... - Svilen
1
@Svilen 为什么你需要将它展平呢?Map<String,Object> 对我来说已经足够了。 - siledh

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