У меня есть простой набор классов, которые я хочу сериализовать в /deserialize из YAML с помощью Jackson (2.4.5):
public static class Animal {
public String name;
}
public static class Dog extends Animal {
public String breed;
}
public static class Cat extends Animal {
public String favoriteToy;
}
public static class AnimalRegistry {
private Map<String, Animal> fAnimals = new HashMap<>();
public AnimalRegistry(Animal... animals) {
for (Animal animal : animals)
fAnimals.put(animal.name, animal);
}
public Animal getAnimal(String name) {
return fAnimals.get(name);
}
}
Это довольно просто сделать так, чтобы AnimalRegistry
в список вложенных объектов объектов Animal
(subtype). Я могу писать и читать их просто отлично. Проблема, с которой я сталкиваюсь, заключается в том, чтобы отдельно сериализовать/десериализовать объекты другого класса:
public static class PetOwner {
public String name;
public List<Animal> pets = new ArrayList<>();
}
Я не хочу сериализовать объекты Animal
как список вложенных объектов, а скорее хранить список имен Animal
. При десериализации я хочу сопоставить эти имена с объектами Animal
с использованием ранее существовавшего AnimalRegistry
.
С JAXB я мог бы просто сделать это с помощью XmlAdapter
:
public static class PetXmlAdapter extends XmlAdapter<String, Animal> {
private AnimalRegistry fRegistry;
public PetXmlAdapter(AnimalRegistry registry) {
fRegistry = registry;
}
@Override
public Animal unmarshal(String value) throws Exception {
return fRegistry.getAnimal(value);
}
@Override
public String marshal(Animal value) throws Exception {
return value.name;
}
}
Я бы аннотировал поле для pets
@XmlJavaTypeAdapter(value = PetXmlAdapter.class)
и добавьте экземпляр PetXmlAdapter
в Marshaller
/Unmarshaller
:
marshaller.setAdapter(new PetXmlAdapter(animalRegistry));
...
unmarshaller.setAdapter(new PetXmlAdapter(animalRegistry));
Джексон поддерживает аннотации JAXB и может использовать один и тот же класс PetXmlAdapter
, но я не вижу способа установить его экземпляр в ObjectMapper
или любом связанном классе и, следовательно, не могу использовать свой ранее существовавший AnimalRegistry
.
У Джексона, похоже, много очков для настройки, и в конце концов я нашел способ достичь своей цели:
public static class AnimalNameConverter
extends StdConverter<Animal, String> {
@Override
public String convert(Animal value) {
return value != null ? value.name : null;
}
}
public static class NameAnimalConverter
extends StdConverter<String, Animal> {
private AnimalRegistry fRegistry;
public NameAnimalConverter(AnimalRegistry registry) {
fRegistry = registry;
}
@Override
public Animal convert(String value) {
return value != null ? fRegistry.getAnimal(value) : null;
}
}
public static class AnimalSerializer
extends StdDelegatingSerializer {
public AnimalSerializer() {
super(Animal.class, new AnimalNameConverter());
}
private AnimalSerializer(Converter<Object,?> converter,
JavaType delegateType,
JsonSerializer<?> delegateSerializer) {
super(converter, delegateType, delegateSerializer);
}
@Override
protected StdDelegatingSerializer withDelegate(
Converter<Object, ?> converter, JavaType delegateType,
JsonSerializer<?> delegateSerializer) {
return new AnimalSerializer(converter, delegateType,
delegateSerializer);
}
}
public static class AnimalDeserializer
extends StdDelegatingDeserializer<Animal> {
private static final long serialVersionUID = 1L;
public AnimalDeserializer(AnimalRegistry registry) {
super(new NameAnimalConverter(registry));
}
private AnimalDeserializer(Converter<Object, Animal> converter,
JavaType delegateType,
JsonDeserializer<?> delegateDeserializer) {
super(converter, delegateType, delegateDeserializer);
}
@Override
protected StdDelegatingDeserializer<Animal> withDelegate(
Converter<Object, Animal> converter,
JavaType delegateType,
JsonDeserializer<?> delegateDeserializer) {
return new AnimalDeserializer(converter, delegateType,
delegateDeserializer);
}
}
public static class AnimalHandlerInstantiator
extends HandlerInstantiator {
private AnimalRegistry fRegistry;
public AnimalHandlerInstantiator(AnimalRegistry registry) {
fRegistry = registry;
}
@Override
public JsonDeserializer<?> deserializerInstance(
DeserializationConfig config, Annotated annotated,
Class<?> deserClass) {
if (deserClass != AnimalDeserializer.class)
return null;
return new AnimalDeserializer(fRegistry);
}
@Override
public KeyDeserializer keyDeserializerInstance(
DeserializationConfig config, Annotated annotated,
Class<?> keyDeserClass) {
return null;
}
@Override
public JsonSerializer<?> serializerInstance(
SerializationConfig config, Annotated annotated,
Class<?> serClass) {
return null;
}
@Override
public TypeResolverBuilder<?> typeResolverBuilderInstance(
MapperConfig<?> config, Annotated annotated,
Class<?> builderClass) {
return null;
}
@Override
public TypeIdResolver typeIdResolverInstance(
MapperConfig<?> config,
Annotated annotated, Class<?> resolverClass) {
return null;
}
}
Я комментирую поле для pets
@JsonSerialize(contentUsing = AnimalSerializer.class)
@JsonDeserialize(contentUsing = AnimalDeserializer.class)
и установить экземпляр AnimalHandlerInstantiator
на ObjectMapper
:
mapper.setHandlerInstantiator(
new AnimalHandlerInstantiator(animalRegistry));
Это работает, но это очень много кода. Может ли кто-нибудь предложить более краткий вариант? Я хотел бы избежать написания сериализатора/десериализатора для PetOwner
который требует ручной обработки полей, отличных от pets
.
Если я правильно прочитаю ваш вопрос, ваши сериализованные записи PetOwner не должны содержать полные записи Animal - они просто должны содержать строку для каждой записи Animal. И ваши осложнения исходят из того факта, что вы тем не менее сохраняете записи Animal.
Предполагая, что для точной формулировки один подход будет заключаться в том, чтобы аннотировать ваше поле "домашние животные" (или геттер, не уверен, как выглядит ваш полный класс PetOwner) с помощью @JsonIgnore, затем добавьте геттер, например:
public List<String> getAnimalNames() {
// return a list containing the name of each Animal in this.pets
}
Затем вы можете:
List<String>
а не List<Animal>
, и аннотирует этот конструктор с помощью @JsonCreator, чтобы Джексон знал его использовать (и создайте экземпляры Animal из предоставленных имен внутри конструктора) public void setAnimalNames(List<String> names) {//populate this.pets by looking up each pet by name in your registry }
Мне нужно будет увидеть больше вашего класса PetOwner и/или узнать больше о том, как вы планируете использовать его, чтобы дать более подробный ответ, но я думаю, что этот общий подход (аннотировать класс PetOwner, чтобы Джексон просто игнорировал "домашних животных" и делал только в 'animalNames') должен дать вам то, что вы ищете.
PetOwner
- это просто класс данных. Он не имеет (и не должен получать) доступ к контексту (AnimalRegistry
), который позволил бы ему сопоставлять имена животных с животными. Так что, к сожалению, ни одно из ваших предложений не сработает для меня.