Джексон: Как я могу постобработать JsonNode во время сериализации? - PullRequest
1 голос
/ 09 октября 2019

Я пытаюсь реализовать утверждение спецификации HL7 FHIR о том, что JSON, представляющий модель FHIR, не будет иметь ни пустых объектов, ни пустых массивов . Чтобы не усложнять жизнь моих потребителей, я не строго соблюдаю это во время десериализации, но хочу убедиться, что сериализованный JSON, созданный моей библиотекой, соответствует указанному. Я использую Java и Джексон ObjectMapper для сериализации объектов в JSON. Насколько я понимаю из написания пользовательского сериализатора, в одном месте объект представлен как JsonNode, независимо от того, во что вы конвертируете.

Я хотел бы перехватить JsonNode при выходе из сериализатора, сделатьвнесите в него некоторые изменения (найдите и удалите пустые массивы и объекты), а затем позвольте ему продолжать свой путь. Мне нужно сделать это в среде, где я не могу настроить ObjectMapper, потому что у меня нет доступа к нему. И далее, сложная иерархия моделей в этой библиотеке использует сериализацию по умолчанию Джексона с аннотациями и т. Д., И я не могу устранить это.

Если я пойду по пути определения пользовательского сериализатора для базового типа, скажем, «Ресурс», то у меня возникнет проблема, потому что мне все еще нужны выходные данные оригинального сериализатора для генерации моего измененного вывода. И, кроме того, это необходимо для размещения любых пользовательских сериализаторов, которые могут уже существовать для различных типов в модели.

Я довольно далеко справился с вышеуказанным параметром, используя https://www.baeldung.com/jackson-call-default-serializer-from-custom-serializer, и последним вариантом, реализующим BeanSerializerModifier, но я столкнулся с проблемой, когда я не могу контролировать ObjectMapper, который используют мои пользователи библиотеки.

Пример POJO (с использованием Lombok для средств доступа):

@Data
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonIgnoreProperties(ignoreUnknown = true)
abstract class Resource {
  private FhirString id;
  private List<Extension> extension;

  @JsonProperty(access = JsonProperty.Access.READ_ONLY)
  public abstract ResourceType getResourceType();
}
@Data
@Builder
class SomethingElse extends Resource {
  FhirUri someProperty;
  CodeableConcept someCode;
  List<Reference> someReferences;

  @Override
  public ResourceType getResourceType() {
    return ResourceType.SOMETHING_ELSE;
  }
}

И пример экземпляра класса SomethingElse:

SomethingElse somethingElse = SomethingElse.builder()
    .someProperty(FhirUri.from("some-simple-uri"))
    .someCode(new CodeableConcept())
    .someReference(List.of(new Reference()))
    .build();
somethingElse.setId(FhirString.randomUuid());
somethingElse.setExtension(new ArrayList<>());

Когда я говорю любому мапперу(или, например, используйте сервис Spring) для сопоставления класса SomethingElse с JsonNode, я могу, например, получить пустые объекты и массивы, например:

ObjectMapper mapper = getUntouchableMapper();
JsonNode somethingElseNode = mapper.valueToTree(somethingElse);
System.out.println(somethingElseNode.toString());

Становится:

{
  "resourceType": "SomethingElse",
  "id": "00000000-0002-0004-0000-000000000000",
  "someProperty": "some-simple-uri",
  "someCode": {},
  "someReferences": [{}],
  "extension": []
}

Согласно FHIR, это на самом деле должно выглядеть следующим образом:

{
  "resourceType": "SomethingElse",
  "id": "00000000-0002-0004-0000-000000000000",
  "someProperty": "some-simple-uri"
}

Подводя итог

Как сохранить уже существующие механизмы сериализации,независимо от используемого ObjectMapper и каким-либо образом удалять пустые списки и объекты из исходящего JSON, созданного в процессе сериализации Джексона?

Редактировать: Я также попробовал @JsonInclude(JsonInclude.Include.NON_EMPTY), в котором не были реализованы пустые списки. Однако подавляющее большинство данных в этой библиотеке представлено POJO, которые сериализуются в карты и примитивы, и эта аннотация работает только в том случае, если они представлены непосредственно в картах и ​​примитивах в модели.

1 Ответ

1 голос
/ 10 октября 2019

Решение состоит в том, чтобы использовать пользовательский @JsonInclude, который является новым в Jackson 2.9 . Спасибо @dai за то, что указал мне на эту функциональность.

Для базового класса Resource это выглядит следующим образом:

@JsonInclude(value = JsonInclude.Include.CUSTOM, valueFilter = FhirJsonValueFilter.class)
class Resource implements FhirTypeInterface {
  ...

  @Override
  public boolean isEmpty() {
    //Details omitted for simplicity
  }
}

Для наглядности интерфейс, использованный выше:

interface FhirTypeInterface {
  boolean isEmpty();
}

И мое пользовательское определение для FhirJsonValueFilter реализует всефункциональности JsonInclude.Include.NON_EMPTY, но также добавляет функциональность для проверки по методу, реализованному типами FHIR (реализация этого не имеет отношения к ответу).

public class FhirJsonValueFilter {
    @Override
    public boolean equals(Object value) {
        return !getWillInclude(value);
    }

    /**
     * Returns true for an object that matched filter criteria (will be 
     * included) and false for those to omit from the response.
     */
    public boolean getWillInclude(Object value) {
        //Omit explicit null values
        if (null == value) {
            return false;
        }

        //Omit empty collections
        if (Collection.class.isAssignableFrom(value.getClass())) {
            return !((Collection) value).isEmpty();
        }

        //Omit empty maps
        if (Map.class.isAssignableFrom(value.getClass())) {
            return !((Map) value).isEmpty();
        }

        //Omit empty char sequences (Strings, etc.)
        if (CharSequence.class.isAssignableFrom(value.getClass())) {
            return ((CharSequence) value).length() > 0;
        }

        //Omit empty FHIR data represented by an object
        if (FhirTypeInterface.class.isAssignableFrom(value.getClass())) {
            return !((FhirTypeInterface) value).isEmpty();
        }

        //If we missed something, default to include it
        return true;
    }
}

Обратите внимание, что пользовательский фильтр пропусков использует функциональность Java Object.equals, где true означает пропуск свойства, и я использовал второй метод, чтобы уменьшить путаницу в этом ответе,

...