Может ли Джексон проверить наличие дублированных свойств без учета регистра? - PullRequest
1 голос
/ 27 марта 2019

Я использую Jackson JSON для преобразования некоторых объектов JSON в классы POJO. Эта десериализация должна быть без учета регистра и не должна допускать атрибутов с без учета регистра повторяющихся имен.

Конфигурирование ObjectMapper, как показано ниже, включает десериализацию без учета регистра и сбой атрибутов, которые имеют строго одинаковое имя:

final ObjectMapper objectMapper;
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);

Однако он не завершается ошибкой, когда входные данные содержат два атрибута с одинаковыми именами и разными регистрами, например, следующие:

{
   "Name": "name01",
   "NAME": "name02"
}

Есть ли способ настроить ObjectMapper на сбой при таких обстоятельствах?

1 Ответ

1 голос
/ 28 марта 2019

Из STRICT_DUPLICATE_DETECTION документации:

Функция, определяющая, будет ли JsonParser явно проверять, повторяющихся имен полей JSON Object не обнаружено. Если включено, Парсер проверит все имена в контексте и сообщит дубликаты бросая JsonParseException; если отключено, парсер не сделает такого проверка. В последнем случае предполагается, что вызывающий абонент заботится о обработка дубликатов на более высоком уровне: привязка данных, например , имеет функции для определения обнаружения, которые будут сделаны там. Обратите внимание, что включение эта функция повлечет за собой снижение производительности из-за необходимости хранить и проверьте дополнительную информацию: это обычно добавляет 20-30% к время выполнения основного синтаксического анализа.

JSON по умолчанию чувствителен к регистру, и это одна из основных причин, по которой его нечувствительность не включена по умолчанию в Jackson. Но мы можем расширить базовую реализацию и добавить проверку. Нам нужно расширить com.fasterxml.jackson.databind.deser.BeanDeserializerModifier и com.fasterxml.jackson.databind.deser.BeanDeserializer, которые десериализуют POJO классов. Приведенное ниже решение зависит от версии, которую вы используете, потому что я скопировал некоторый код из базового класса, который не готов к перехвату дополнительной функциональности. Если у вас нет дополнительной конфигурации для вашего метода POJO classes vanillaDeserialize, будет вызван этот метод, который мы постараемся улучшить. Давайте реализуем это:

class InsensitiveBeanDeserializerModifier extends BeanDeserializerModifier {

    @Override
    public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
        JsonDeserializer<?> base = super.modifyDeserializer(config, beanDesc, deserializer);
        if (base instanceof BeanDeserializer) {
            return new InsensitiveBeanDeserializer((BeanDeserializer) base);
        }

        return base;
    }
}

class InsensitiveBeanDeserializer extends BeanDeserializer {

    public InsensitiveBeanDeserializer(BeanDeserializerBase src) {
        super(src);
    }

    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        // common case first
        if (p.isExpectedStartObjectToken()) {
            if (_vanillaProcessing) {
                return vanillaDeserialize(p, ctxt, p.nextToken());
            }
            // 23-Sep-2015, tatu: This is wrong at some many levels, but for now... it is
            //    what it is, including "expected behavior".
            p.nextToken();
            if (_objectIdReader != null) {
                return deserializeWithObjectId(p, ctxt);
            }
            return deserializeFromObject(p, ctxt);
        }
        return _deserializeOther(p, ctxt, p.getCurrentToken());
    }

    protected Object vanillaDeserialize(JsonParser p, DeserializationContext ctxt, JsonToken t) throws IOException {
        final Object bean = _valueInstantiator.createUsingDefault(ctxt);
        // [databind#631]: Assign current value, to be accessible by custom serializers
        p.setCurrentValue(bean);
        Map<String, String> names = new HashMap<>();
        if (p.hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
            String propName = p.getCurrentName();
            do {
                String oldName = names.put(propName.toLowerCase(), propName);
                if (oldName != null) {
                    String msg = "Properties '" + propName + "' and '" + oldName + "' are the same!";
                    throw new DuplicateInsensitiveKeysException(p, msg);
                }

                defaultImplementation(p, ctxt, bean, propName);
            } while ((propName = p.nextFieldName()) != null);
        }
        return bean;
    }

    private void defaultImplementation(JsonParser p, DeserializationContext ctxt, Object bean, String propName) throws IOException {
        p.nextToken();
        SettableBeanProperty prop = _beanProperties.find(propName);

        if (prop != null) { // normal case
            try {
                prop.deserializeAndSet(p, ctxt, bean);
            } catch (Exception e) {
                wrapAndThrow(e, bean, propName, ctxt);
            }
            return;
        }
        handleUnknownVanilla(p, ctxt, bean, propName);
    }

    public static class DuplicateInsensitiveKeysException extends JsonMappingException {

        public DuplicateInsensitiveKeysException(Closeable processor, String msg) {
            super(processor, msg);
        }
    }
}

Пример использования:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.JsonTokenId;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.module.SimpleModule;

import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        File jsonFile = new File("./resource/test.json").getAbsoluteFile();

        SimpleModule module = new SimpleModule();
        module.setDeserializerModifier(new InsensitiveBeanDeserializerModifier());

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(module);

        mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);

        System.out.println(mapper.readValue(jsonFile, User.class));
    }
}

Для вышеупомянутых JSON распечаток полезных нагрузок:

Exception in thread "main" InsensitiveBeanDeserializer$DuplicateInsensitiveKeysException: Properties 'NAME' and 'Name' are the same!
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...