Сопоставление одного настраиваемого поля Java со многими полями JSON с использованием Jackson @JsonDeserializer - PullRequest
0 голосов
/ 05 апреля 2019

У меня есть класс Java, представляющий JSON с использованием Джексона. Все поля, за одним исключением, могут быть переведены без каких-либо аннотаций. 1-к-1, простые переводы (хотя некоторые из них являются вложенными POJO).

@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyPojo {
    private String someString;
    private AnotherPojo someOtherPojo;
    //The problem child:
    private Object value;
}

Поле value, которое является исключением из этого правила, может представлять любое поле JSON, соответствующее value*, где * - это подстановочный знак неопределенной длины. Это означает, что valueString или valueReference в JSON будут присвоены этому полю с утверждением, что может присутствовать только один.

{
  "someString": "asdasdadasdsa",
  "someOtherPojo": {
    "someOtherProperty": "whatever"
  },
  "valueCodeableConcept": {
    "text": "text value",
    "coding": [
      {
        "code": "my-code"
      }
    ]
  }
}

Используя пользовательский десериализатор в классе верхнего уровня, я могу очистить все поля корневого узла (baseNode в следующем примере), которые начинаются с value, и соответствующим образом установить поле значения. Это прекрасно работает! Однако при этом мне теперь нужно вручную установить все остальные поля в этом классе MyPojo в моем десериализаторе, и я должен поместить пользовательскую копию этого десериализатора в каждый POJO, который использует поле типа value*.

private Object parseValueX(JsonNode baseNode, DeserializationContext context) throws IOException {
    //Find the concrete implementation referred to by the value[x] field
    Set<String> concreteNames = new HashSet<>();
    baseNode.fieldNames().forEachRemaining(name -> {
        if (name.startsWith("value")) {
            concreteNames.add(name);
        }});

    if (concreteNames.isEmpty()) {
        return null;
    }

    if (concreteNames.size() > 1) {
        throw JsonMappingException.from(context, "The field value[x] must have no more than one concrete " +
                "implementation, ex: valueCode, valueCodeableConcept, valueReference");
    }

    String concreteName = concreteNames.stream().findFirst().orElseThrow(() -> new RuntimeException(""));
    JsonNode jsonSource = baseNode.get(concreteName);

    //...deserialize from jsonSource, solved, but not relevant to question...
}

Чтобы применить это к любому свойству value* любого POJO, я попытался переместить десериализатор в атрибут value в POJO (тогда как он сейчас находится на ресурсе верхнего уровня). Первый недостаток заключается в том, что десериализатор даже не вызывается, если свойство JSON точно не соответствует value. Что мне действительно нужно, так это чтобы весь родительский ресурс JSON был передан в этот десериализатор для конкретного поля, чтобы я мог найти соответствующее поле и назначить его - ИЛИ - мне нужно, чтобы десериализатор был включен в MyPojo только назначьте одно поле value и разрешите автоматической десериализации позаботиться о других. Как мне сделать что-либо из этого?


Для тех, кто интересуется моей мотивацией, я реализую спецификацию HL7 FHIR, которая задает общие атрибуты, называемые value [x] (вот один пример: https://www.hl7.org/fhir/extensibility.html#Extension), где [x] становится типом ресурса.

1 Ответ

1 голос
/ 07 апреля 2019

Я думаю, что вам подходит проблема @ JsonAnySetter . Этот метод аннотации говорит Джексону направить неизвестные свойства к нему. arg (в вашем случае) - это карта, содержащая дерево json с неизвестным свойством. если я правильно понимаю ваш код, имя свойства value содержит имя класса целевого Pojo. поэтому, когда у вас есть имя класса, вы можете сказать Джексону, как «десериализовать» карту в экземпляр целевого класса.

Вот пример, основанный на коде из вопроса

public class MyPojo {
    public String someString;  // made properties into public for this example...
    public AnotherPojo someOtherPojo;
    public Object value;

    @JsonAnySetter
    public void setValue(String name, Object value) {
        System.out.println(name + " " + value.getClass());
        System.out.println(value);

        // basic validation
        if (name.startsWith("value") && value instanceof Map) {
            String className = "com.company." + name.substring("value".length());
            System.out.println(name + " " + value.getClass() + " " + className);
            System.out.println(value);
            try {
                // nice of Jackson to be able to deserialize Map into Pojo :)
                ObjectMapper mapper = new ObjectMapper();
                this.value = mapper.convertValue(value, Class.forName(className));
            } catch (Exception e) {
                e.printStackTrace();
            }
            System.out.println(this.value + " " + this.value.getClass());
        }

    }
}

public class AnotherPojo {
    public String someOtherProperty;
}

public class CodeableConcept {
    public String text;
    public Code[] coding;
}

public class Code {
    public String code;
}
...