Как я могу восстановить карту объектов из 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, который принимает ссылку на тип (в отличие от 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());
}
Другие вопросы по тегам