Как я могу восстановить карту объектов из JSON?
У меня есть этот класс
public static class SomeClass {
public SomeClass(String field) {
this.field = field;
}
private final String field;
public String getField() {
return field;
}
}
У меня тоже этот тест (отредактированный)
@Test
public void testStringifyMapOfObjects() {
Map<String, SomeClass> original = Maps.newTreeMap();
original.put("first", new SomeClass("a"));
original.put("second", new SomeClass("b"));
String encoded = JsonUtil.toJson(original);
Map<String, SomeClass> actual = JsonUtil.fromJson(encoded, Map.class);
Assert.assertEquals("{'first':{'field':'a'},'second':{'field':'b'}}", encoded.replaceAll("\\s", "").replaceAll("\"", "'"));
Assert.assertEquals(original.get("first"), actual.get("first"));
}
Тест не проходит с
junit.framework.AssertionFailedError: expected:<eu.ec.dgempl.eessi.facade.transport.test.TestToolTest$SomeClass@6e3ed98c> but was:<{field=a}>
at junit.framework.Assert.fail(Assert.java:47)
at junit.framework.Assert.failNotEquals(Assert.java:277)
at junit.framework.Assert.assertEquals(Assert.java:64)
at junit.framework.Assert.assertEquals(Assert.java:71)
at eu.ec.dgempl.eessi.facade.transport.test.TestToolTest.testStringifyMapOfObjects(TestToolTest.java:90)
Могу ли я сделать json для правильной сериализации объектов в качестве значений карты или я должен использовать что-то еще?
отредактированный
public class JsonUtil {
private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(JsonUtil.class);
public static <T> String toJson(T data) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(Feature.INDENT_OUTPUT, true);
try {
return mapper.writeValueAsString(data);
} catch (IOException e) {
LOG.warn("can't format a json object from [" + data + "]", e);
return null;
}
//
// return Json.stringify(Json.toJson(data));
}
public static <T> T fromJson(String description, Class<T> theClass) {
try {
JsonNode parse = new ObjectMapper().readValue(description, JsonNode.class);
T fromJson = new ObjectMapper().treeToValue(parse, theClass);
return fromJson;
} catch (JsonParseException e) {
// throw new RuntimeException("can't parse a json object of type " + theClass.getName() + " from [" + description + "]", e);
LOG.warn("can't parse a json object from [" + description + "]", e);
return null;
} catch (JsonMappingException e) {
// throw new RuntimeException("can't parse a json object of type " + theClass.getName() + " from [" + description + "]", e);
LOG.warn("can't parse a json object from [" + description + "]", e);
return null;
} catch (IOException e) {
// throw new RuntimeException("can't parse a json object of type " + theClass.getName() + " from [" + description + "]", e);
LOG.warn("can't parse a json object from [" + description + "]", e);
return null;
}
}
}
2 ответа
Вы столкнулись с проблемой, связанной с обобщениями Java. Подводя итог, можно сказать, что при десериализации данных в тип без повторного ввода (он же тип, для которого фактическая информация о типе недоступна во время выполнения) необходимо использовать маркер супертипа. Вы можете получить более подробную информацию о том, что такое токен супертипа (и почему вы должны его использовать), прочитав эти сообщения SO:
- Передать параметризованный тип методу в качестве аргумента
- Ошибка при использовании Джексона и JSON
- Десериализовать JSON в ArrayList, используя Джексона
А также из документации Джексона:
Основная проблема заключается в том, что при использовании типового универсального объекта фактические параметры типа для объекта недоступны во время выполнения. Поэтому Джексон не знает, к какому классу нужно создать и десериализовать ваши данные.
Самый простой способ обойти эту проблему - добавить перегрузку в свой служебный класс JSON, который принимает ссылку на тип (в отличие от Class<T>
). Например:
public static <T> T fromJson(String json, TypeReference<T> typeRef) {
if(json == null || typeRef == null) return null;
return new ObjectMapper().readValue(json, typeRef);
}
Для использования как таковой:
Map<String, SomeClass> actual = JsonUtil.fromJson(
encoded,
new TypeReference<Map<String, SomeClass>>(){});
Я обнаружил, что самое простое решение - создать класс "контейнер", который будет содержать карту. Это работает, вероятно, потому, что в контейнере достаточно сведений о типе карты, в отличие от случая, когда карта используется напрямую.
public static class SomeClass {
private final String field;
private SomeClass() {
this("wrong");
}
public SomeClass(String field) {
this.field = field;
}
public String getField() {
return field;
}
@Override
public String toString() {
return "SomeClass[" + field + "]";
}
}
public static class SomeClassContainer {
private final Map<String, SomeClass> all = Maps.newTreeMap();
public Map<String, SomeClass> getAll() {
return all;
}
}
После этого... обновленный тест
@Test
public void testStringifyMapOfObjects() {
SomeClassContainer original = new SomeClassContainer();
original.getAll().put("first", new SomeClass("a"));
original.getAll().put("second", new SomeClass("b"));
String encoded = JsonUtil.toJson(original);
System.out.println(encoded);
SomeClassContainer actual = JsonUtil.fromJson(encoded, SomeClassContainer.class);
System.out.println(ObjectUtils.toString(actual));
Assert.assertEquals("{'all':{'first':{'field':'a'},'second':{'field':'b'}}}", encoded.replaceAll("\\s", "").replaceAll("[\"]", "'"));
Assert.assertEquals("class eu.ec.dgempl.eessi.facade.transport.test.TestToolTest$SomeClass", actual.getAll().get("first").getClass().toString());
Assert.assertEquals(original.getAll().get("first").toString(), actual.getAll().get("first").toString());
Assert.assertEquals(original.getAll().get("second").toString(), actual.getAll().get("second").toString());
}