Как десериализовать JSON в плоскую структуру, похожую на карту?

Имейте в виду, что структура 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 по сути является картой вложенных карт:

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

Хотя мне нужна плоская карта с плоскими клавишами, использующими "точечную нотацию", как показано выше.

Сам бы не стал реализовывать это, хотя на данный момент другого пути не вижу ...


person Svilen    schedule 03.12.2013    source источник
comment
Джексон (или любая другая библиотека JSON) может преобразовать JSON в карту карт. Сделать лишнюю милю нетривиально, и пример синтаксиса, который вы показываете, никогда не может быть сгенерирован во время выполнения в java. Вы можете добиться чего-то похожего на то, что вам нужно, с помощью библиотеки Typesafe Config.   -  person Giovanni Botta    schedule 03.12.2013
comment
Хорошо, поэтому я сделал Config parseString = ConfigFactory.parseString (portJsonString); То, что toString () выглядит примерно так: Config (SimpleConfigObject ({Port: {Enabled: false, Number: 10033, Type: DIAGNOSTIC, @ alias: diagnostics, ExtendedProperties: {Property: {@ name: suspend, $: n}}), Протокол: JDWP}})) Но я не уверен, как сгладить это с помощью библиотеки Typesafe Config?   -  person Svilen    schedule 03.12.2013
comment
Добавил ответ. Надеюсь, это поможет.   -  person Giovanni Botta    schedule 03.12.2013


Ответы (7)


Вы можете сделать это, чтобы пройти по дереву и отслеживать, насколько глубоко вы понимаете имена свойств с точечной нотацией:

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 был произвольным.

person Harleen    schedule 10.06.2014
comment
В конце концов, я выбрал собственное решение, очень похожее на это, поэтому пометил его как принятый ответ. Спасибо! - person Svilen; 03.04.2016

Как насчет использования 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"
}
person user3360932    schedule 05.08.2015
comment
действительно хорошая работа. Надеюсь, вы продолжите поддерживать его и готово ли к производству. Хорошо, если вы также можете публиковать показатели производительности и тестового покрытия в репозитории на github. ты все еще активно его развиваешь? - person Ashish Thukral; 01.03.2017
comment
могу ли я преобразовать карту в json? - person PDS; 06.07.2017
comment
Работает относительно хорошо, НО это преобразовывает мои реквизиты Integer в BigDecimal, что очень уродливо и бесполезно для меня, потому что заставляет дополнительные процедуры повторно преобразовывать типы из BigDecimal. - person eriknyk; 26.09.2019

как насчет этого:

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());
}
}
person Amnons    schedule 25.01.2015

org.springframework.integration.transformer .ObjectToMapTransformer из Spring Integration дает желаемый результат. По умолчанию для свойства shouldFlattenKeys установлено значение true, и он создает плоские карты (без вложенности, значение всегда имеет простой тип). Когда shouldFlattenKeys=false он создает вложенные карты

ObjectToMapTransformer предназначен для использования как часть процесса интеграции, но вполне нормально использовать его автономно. Вам нужно построить org.springframework.messaging.Message с полезной нагрузкой ввода преобразования. transform метод возвращает 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 в путь к классам только для использования одного класса, но вы можете проверить реализацию этого класса и написать аналогичное решение самостоятельно. Вложенная карта создается Джексоном (org.springframework.integration.support.json.JsonObjectMapper#fromJson(payload, Map.class)), затем карта обрабатывается рекурсивно, сглаживая все значения, являющиеся коллекциями.

person Bartosz Bilicki    schedule 18.04.2016
comment
Последняя строка может быть намного проще: Map payload = transformer.doTransform(message); - person Vikas Prasad; 16.07.2017

Вы можете добиться чего-то подобного с помощью библиотеки конфигурации Typesafe, как показано в следующем примере:

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);
    }
  }
}
person Giovanni Botta    schedule 03.12.2013
comment
Спасибо, Джованни. Хотя это действительно шаг ближе к тому, что мне нужно, этого все же недостаточно. Например, он создает такие вещи, как: Port. @ Alias ​​= ConfigString (defaultHttp) и Port.ExtendedProperties.Property = SimpleConfigList ([{@ name: jaasRealm, $: PlatformManagement}]) - явно не сглаженный набор свойств. Итак, если я хочу использовать это, мне нужно будет дополнительно проанализировать вывод библиотеки Typesafe ... Думаю, я просто воспользуюсь своим собственным решением и опубликую его здесь, когда это будет сделано. - person Svilen; 11.12.2013
comment
Что ж, то, что вы пытаетесь сделать, - это очень нестандартная вещь, потому что у вас есть имена полей в стиле аннотации, которые представляют собой нечто иное. Насколько я знаю, это не стандартная концепция JSON или что-то еще, поэтому вам нужно будет немного поработать здесь. Я надеюсь, что библиотека Typesafe поможет. - person Giovanni Botta; 11.12.2013
comment
Вам просто нужно выполнить дополнительную обработку, если вам нужен другой формат, вместо простого вызова toString. Каждая запись имеет выражение пути, которое при желании можно разделить на ConfigUtil.splitPath, а ConfigValue можно преобразовать в обычное значение Java с помощью value.unwrapped(). Учитывая разделенный путь, вы можете собрать его, используя желаемый синтаксис. - person Havoc P; 11.01.2014
comment
@Svilen Ты это решил? Не могли бы вы опубликовать свое решение, так как у меня сейчас такая же проблема? - person Battle_Slug; 10.09.2014
comment
В итоге я реализовал его сам, используя API Джексона. В основном используйте Jackson для синтаксического анализа json на карту (карт и списков), а затем рекурсивно просматривайте ее и строите плоскую карту. Подобно siledh answer - person Svilen; 23.09.2014

Если вы заранее знаете структуру, вы можете определить класс Java и использовать gson для синтаксического анализа JSON в экземпляр этого класса:

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

Если нет, то я не уверен, что вы пытаетесь сделать. Очевидно, вы не можете определить класс на лету, поэтому о доступе к проанализированному JSON с использованием точечной нотации не может быть и речи.

Если вы не хотите чего-то вроде:

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

Если так, то перемещение по карте карт и построение «плоской» карты не кажется большой проблемой.

Или что-то еще?

person siledh    schedule 03.12.2013
comment
Модель заранее не известна. Идея состоит в том, чтобы передать этот сглаженный JSON в службу сравнения, которая, ну, чтобы сравнить его с другим сглаженным таким же образом json. Проблема в том, что служба ожидает определенного формата, то есть этой уплощенной вещи, которую я пытаюсь достичь ... Я думал, что это не должно быть такой необычной проблемой, поэтому решение уже должно быть доступно в какой-то библиотеке. - person Svilen; 03.12.2013
comment
@Svilen Почему нельзя просто сравнить сами строки JSON? - person siledh; 03.12.2013
comment
siledh, причина в том, что я хотел бы иметь простой способ изменить только одно значение свойства / листа после того, как было выполнено различие. Мне было бы намного сложнее сделать это, имея простой текстовый diff ... - person Svilen; 10.12.2013
comment
@Svilen Зачем тогда это нужно сглаживать? Map ‹String, Object› мне кажется достаточным. - person siledh; 11.12.2013

Мне также пришлось решить аналогичную проблему в моем проекте, и я обнаружил, что в 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

person Roopashree    schedule 07.02.2021