Exclude classes and namespaces from serialization? - java

I'm trying to serialize a rather large structure with Jackson.
However, it's also trying to export a lot of substructures I will never need (causing a JsonMappingException: No serializer found for class)
So how can I exclude classes and namespaces from serialization?
Alternatively, how can I flag properties of my classes as excluded/ignored?

Use the transient keyword if you have actually access to the substructure you want to exclude.
transient is a Java keyword which marks a member variable not to be
serialized when it is persisted to streams of bytes. When an object is
transferred through the network, the object needs to be 'serialized'.
Serialization converts the object state to serial bytes. Those bytes
are sent over the network and the object is recreated from those
bytes. Member variables marked by the java transient keyword are not
transferred, they are lost intentionally.
http://en.wikibooks.org/wiki/Java_Programming/Keywords/transient

Please give an example for exclude classes and namespace but for properties for which you might not control the source code you can use the following on types and fields
#JsonIgnoreProperties(value = {"propertyName", "otherProperty"})
Here's the javadoc.
Here's an example
#JsonIgnoreProperties(value = { "name" })
public class Examples {
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
Examples examples = new Examples();
examples.setName("sotirios");
Custom custom = new Custom();
custom.setValue("random");
custom.setNumber(42);
examples.setCustom(custom);
ObjectMapper mapper = new ObjectMapper();
StringWriter writer = new StringWriter();
mapper.writeValue(writer, examples);
System.out.println(writer.toString());
}
private String name;
#JsonIgnoreProperties(value = { "value" })
private Custom custom;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Custom getCustom() {
return custom;
}
public void setCustom(Custom custom) {
this.custom = custom;
}
static class Custom {
private String value;
private int number;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
}
prints
{"custom":{"number":42}}
In other words, it ignored Examples#name and Custom#value.

Related

Serializing Java boolean array as 0 & 1s without a custom Serializer

I have a ConcurrentHashMap<Long, boolean[]> that I want to send to a client app. To optimize the size, I'd like to use 0 and 1 for false and true, respectively.
I have an ObjectMapper that I instantiate so I have tried this:
objectMapper.configOverride( boolean.class ).setFormat( JsonFormat.Value.forShape( Shape.NUMBER_INT ) ) ;
That did not make it produce 0 and 1s.
Can this be done using only configuration or will I have to create a custom Serializer?
Thanks
Using this class to test:
public class Sample {
public Sample(String name, boolean flag) {
this.name = name;
this.flag = flag;
}
private String name;
private boolean flag;
public boolean getFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
And trying the OP's approach to convert a Boolean value into a number
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configOverride(boolean.class).setFormat(JsonFormat.Value.forShape(Shape.NUMBER_INT));
Sample sample1 = new Sample("foo", true);
objectMapper.writeValue(new File("sample1.json"), sample1);
Resulted in this:
{"name":"foo","flag":true}
So, the conversion didn't work. The OP indicated the typical 1 and 0 values for true and false respectively. The mistake? .configOverride(Boolean.class) instead of .configOverride(boolean.class) should be used. After that change, sample1.json
is now
{"name":"foo","flag":1}
Although that works, we run into a problem with deserialization. You can't just convert a 1 or 0 into a Boolean. For this, the best approach is to create serializer and deserializer classes that can be used to convert to/from Boolean. Then, these classes must be added as modules to the object mapper.
Deserializer
public class NumericBooleanDeserializer extends JsonDeserializer<Boolean>{
#Override
public Boolean deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
if ("1".equals(p.getText())) {
return Boolean.TRUE;
}
if ("0".equals(p.getText())) {
return Boolean.FALSE;
}
return null;
}
}
Serializer
public class NumericBooleanSerializer extends JsonSerializer<Boolean>{
#Override
public void serialize(Boolean value, JsonGenerator gen, SerializerProvider serializers)
throws IOException {
gen.writeString(value ? "1" : "0");
}
}
Now, we need to make two changes to Sample class. One, add a no-argument contructor, and annotate the boolean field.
public static class Sample {
public Sample() {}
public Sample(String name, boolean flag) {
this.name = name;
this.flag = flag;
}
private String name;
#JsonSerialize(using = NumericBooleanSerializer.class)
#JsonDeserialize(using = NumericBooleanDeserializer.class)
private boolean flag;
// rest of the class omitted
}
Now, if I update my test main() method as follows
ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configOverride(Boolean.class).setFormat(JsonFormat.Value.forShape(Shape.NUMBER_INT));
SimpleModule module = new SimpleModule();
module.addSerializer(Boolean.class, new NumericBooleanSerializer());
module.addDeserializer(Boolean.class, new NumericBooleanDeserializer());
objectMapper.registerModule(module);
Sample sample1 = new Sample("foo", true);
System.out.println("Sample1: " + sample1);
objectMapper.writeValue(new File("sample1.json"), sample1);
Sample sample2 = objectMapper.readValue(Paths.get("sample1.json").toFile(), Sample.class);
System.out.println("Sample2: " + sample2);
The program outputs:
Sample1: { name :foo, flag : true }
Sample2: { name :foo, flag : true }
while the created file sample1.json contains the converted values for Boolean
{"name":"foo","flag":"1"}
UPDATE: For this simple example, the serializer is not really needed. The only difference between using the serializer and when you use the config override, is that the numeric value for the boolean is wrapped in double quotes when the serializer is used. The deserializer does not care about this small detail and it's able to convert the number to Boolean just the same. I just thought it was a good idea to show how to create and use the serializer.

Jackson mixin is ignored on serialization and deserialization

I need to be able to create a Java POJO from a JSON object when I only have an interface that can't be changed. I'm hoping that Mixins can help make this possible. I created a Mixin that hopefully will work but can't get Jackson to use it.
It appears that Jackson is ignoring the Mixin I am defining for both an Interface and an Implementation. The test failures are what I would expect without the Mixin added to the ObjectMapper.
Below is the simplest example that shows the problem. The classes are each in their own package. The real uses case is much more complex, including Lists of interfaces. I am using Jackson 2.10.3.
Any suggestions on what I'm doing wrong?
Timothy
What doesn't work
The interface reader test fails with InvalidDefinitionException: Cannot construct instance of model.Level4 (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
Of secondary importance, the Mixin defines a new label (nameTest) for the name field which should be reflected in the output from writeValueAsString. It outputs the field with the original value for the label (name).
Interface
public interface Level4 {
public Long getId();
public void setId(Long id);
public String getName();
public void setName(String name);
}
Implementation
public class Level4Impl implements Level4 {
private Long id;
private String name;
#Override
public Long getId() {
return id;
}
#Override
public void setId(Long id) {
this.id = id;
}
#Override
public String getName() {
return name;
}
#Override
public void setName(String name) {
this.name = name;
}
}
Mixin
public abstract class Level4Mixin {
public Level4Mixin(
#JsonProperty("id") Long id,
#JsonProperty("nameTest") String name) { }
}
Unit Test
class Level4MixinTest {
private ObjectMapper mapper;
#BeforeEach
void setUp() throws Exception {
mapper = new ObjectMapper();
mapper.addMixIn(Level4.class, Level4Mixin.class);
mapper.addMixIn(Level4Impl.class, Level4Mixin.class);
}
#Test
void test_InterfaceWrite() throws JsonProcessingException {
Level4 lvl4 = new Level4Impl();
lvl4.setId(1L);
lvl4.setName("test");
String json = mapper.writeValueAsString(lvl4);
assertNotNull(json);
assertTrue(json.contains("nameTest"));
}
#Test
void test_InterfaceRead() throws JsonProcessingException {
String json = "{\"id\":1,\"nameTest\":\"test\"}";
assertDoesNotThrow(() -> {
Level4 parsed = mapper.readValue(json, Level4.class);
assertNotNull(parsed);
});
}
#Test
void test_ImplWrite() throws JsonProcessingException {
Level4Impl lvl4 = new Level4Impl();
lvl4.setId(1L);
lvl4.setName("test");
String json = mapper.writeValueAsString(lvl4);
assertNotNull(json);
assertTrue(json.contains("nameTest"));
}
#Test
void test_ImplRead() {
String json = "{\"id\":1,\"nameTest\":\"test\"}";
assertDoesNotThrow(() -> {
Level4Impl parsed = mapper.readValue(json, Level4Impl.class);
assertNotNull(parsed);
});
}
}
First of all you have to let Jackson know which subclass of your interface it should instantiate. You do it by adding #JsonTypeInfo and/or #JsonSubTypes annotations to your mix-in class. For single subclass the following would suffice:
#JsonTypeInfo(use = Id.NAME, defaultImpl = Level4Impl.class)
public abstract class Level4Mixin {
}
For multiple sub-classes it will a bit more complex and will require additional field in JSON payload that will identify concrete type. See Jackson Polymorphic Deserialization for details. Also worth mentioning that adding type info will cause type ID field to be written to JSON. JFYI.
Adding new label would be as trivial as adding a pair of getter and setter for desired property. Obviously original name field will be written to JSON too in this case. To change that you may want to place #JsonIgnore on getter in subclass or in mix-in. In latter case name will be ignored for all sub-classes.
Last note: in this case you should register your mix-in with super-type only.
Here are the changes to your classes that satisfy your tests:
Level4Impl
public class Level4Impl implements Level4 {
private Long id;
private String name;
#Override
public Long getId() {
return id;
}
#Override
public void setId(Long id) {
this.id = id;
}
#Override
public String getName() {
return name;
}
#Override
public void setName(String name) {
this.name = name;
}
public String getNameTest() {
return name;
}
public void setNameTest(String name) {
this.name = name;
}
}
Mixin
#JsonTypeInfo(use = Id.NAME, defaultImpl = Level4Impl.class)
public interface Level4Mixin {
#JsonIgnore
String getName();
}
Level4MixinTest change
#BeforeEach
void setUp() throws Exception {
mapper = new ObjectMapper();
mapper.addMixIn(Level4.class, Level4Mixin.class);
// remove
//mapper.addMixIn(Level4Impl.class, Level4Mixin.class);
}
For adding properties to an object when that object is serialized you can use #JsonAppend. For example:
#JsonAppend(attrs = {#JsonAppend.Attr(value = "nameTest")})
public class Level4Mixin {}
And the test:
#BeforeEach
void setUp() throws Exception {
mapper = new ObjectMapper()
.addMixIn(Level4Impl.class, Level4Mixin.class);
}
#Test
void test_ImplWrite() throws JsonProcessingException {
Level4Impl lvl4 = new Level4Impl();
lvl4.setId(1L);
lvl4.setName("test");
String json = mapper.writerFor(Level4Impl.class)
.withAttribute("nameTest", "myValue")
.writeValueAsString(lvl4);
assertNotNull(json);
assertTrue(json.contains("nameTest"));
assertTrue(json.contains("myValue"));
}
The same works for test_InterfaceWrite.
The tests to deserialize a json into an object are not clear:
#Test
void test_ImplRead() {
String json = "{\"id\":1,\"nameTest\":\"test\"}";
assertDoesNotThrow(() -> {
Level4Impl parsed = mapper.readValue(json, Level4Impl.class);
assertNotNull(parsed);
});
}
The class Level4Impl does not have the property nameTest so the deserialization fails. If you don't want to throw the exception you can configure the ObjectMapper to don't fail on unknown properties. For example:
#Test
void test_ImplRead() {
String json = "{\"id\":1,\"nameTest\":\"test\"}";
assertDoesNotThrow(() -> {
Level4Impl parsed = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.readValue(json, Level4Impl.class);
assertNotNull(parsed);
});
}
In case you can't make it work by default (which was my case), try to modify existing MappingJackson2HttpMessageConverter converter, e.g. do it this way:
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.stream()
.filter(c -> c instanceof MappingJackson2HttpMessageConverter)
.forEach(c -> {
MappingJackson2HttpMessageConverter converter = (MappingJackson2HttpMessageConverter) c;
converter.getObjectMapper().addMixIn(APIResponse.class, MixInAPIResponse.class);
});
}
Where MixInAPIResponse is your configured MixIn class for target class ApiResponse.

Jackson Custom Deserializer for polymorphic objects and String literals as defaults

I'd like to deserialize an object from YAML with the following properties, using Jackson in a Spring Boot application:
Abstract class Vehicle, implemented by Boat and Car
For simplicity, imagine both have a name, but only Boat has also a seaworthy property, while Car has a top-speed.
mode-of-transport:
type: boat
name: 'SS Boatface'
seaworthy: true
----
mode-of-transport:
type: car`
name: 'KITT'
top-speed: 123
This all works fine in my annotated subclasses using #JsonTypeInfo and #JsonSubTypes!
Now, I'd like to create a shorthand using only a String value, which should create a Car by default with that name:
mode-of-transport: 'KITT'
I tried creating my own custom serializer, but got stuck on most of the relevant details. Please help me fill this in, if this is the right approach:
public class VehicleDeserializer extends StdDeserializer<Merger> {
/* Constructors here */
#Override
public Vehicle deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (/* it is an OBJECT */){
// Use the default polymorphic deserializer
} else if (/* it is a STRING */) {
Car car = new Car();
car.setName( /* the String value */ );
return car;
}
return ???; /* what to return here? */
}
}
I found these 2 answers for inspiration, but it looks like combining it with polymorphic types makes it more difficult: How do I call the default deserializer from a custom deserializer in Jackson and Deserialize to String or Object using Jackson
A few things are different than the solutions offered in those questions:
I am processing YAML, not JSON. Not sure about the subtle differences there.
I have no problem hardcoding the 'default' type for Strings inside my Deserializer, hopefully making it simpler.
This was actually easier than I thought to solve it. I got it working using the following:
Custom deserializer implementation:
public class VehicleDeserializer extends StdDeserializer<Vehicle> {
public VehicleDeserializer() {
super(Vehicle.class);
}
#Override
public Vehicle deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
if (jp.currentToken() == JsonToken.VALUE_STRING) {
Car car = new Car();
car.setName(jp.readValueAs(String.class));
return car;
}
return jp.readValueAs(Vehicle.class);
}
}
To avoid circular dependencies and to make the custom deserializer work with the polymorphic #JsonTypeInfo and #JsonSubTypes annotations I kept those annotations on the class level of Vehicle, but put the following annotations on the container object I am deserializing:
public class Transport {
#JsonDeserialize(using = VehicleDeserializer.class)
#JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
private Vehicle modeOfTransport;
// Getter, setters
}
This means that by default a Vehicle is deserialized as a polymorphic object, unless explicitly specified to deserialize it using my custom deserializer. This deserializer will then in turn defer to the polymorphism if the input is not a String.
Hopefully this will help someone running into this issue :)
So there is a solution that requires you to handle the jackson errors using a DeserializationProblemHandler (since you want to parse the same type using different inputs, this is not achieved easily using regular means):
public class MyTest {
#Test
public void doTest() throws JsonParseException, JsonMappingException, IOException {
final ObjectMapper om = new ObjectMapper();
om.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(final DeserializationContext ctxt, final Class<?> instClass, final JsonParser p, final String msg) throws IOException {
if (instClass.equals(Car.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return new Car();
}
}
return NOT_HANDLED;
}
#Override
public JavaType handleMissingTypeId(final DeserializationContext ctxt, final JavaType baseType, final TypeIdResolver idResolver, final String failureMsg) throws IOException {
// if (baseType.isTypeOrSubTypeOf(Vehicle.class)) {
final JsonParser parser = ctxt.getParser();
final String text = parser.getText();
switch (text) {
case "KITT":
return TypeFactory.defaultInstance().constructType(Car.class);
}
return super.handleMissingTypeId(ctxt, baseType, idResolver, failureMsg);
}
});
final Container objectValue = om.readValue(getObjectJson(), Container.class);
assertTrue(objectValue.getModeOfTransport() instanceof Car);
final Container stringValue = om.readValue(getStringJson(), Container.class);
assertTrue(stringValue.getModeOfTransport() instanceof Car);
}
private String getObjectJson() {
return "{ \"modeOfTransport\": { \"type\": \"car\", \"name\": \"KITT\", \"speed\": 1}}";
}
private String getStringJson() {
return "{ \"modeOfTransport\": \"KITT\"}";
}
}
class Container {
private Vehicle modeOfTransport;
public Vehicle getModeOfTransport() {
return modeOfTransport;
}
public void setModeOfTransport(final Vehicle modeOfTransport) {
this.modeOfTransport = modeOfTransport;
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type", visible = true)
#JsonSubTypes({
#Type(name = "car", value = Car.class)
})
abstract class Vehicle {
protected String type;
protected String name;
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
}
#JsonTypeName("car")
class Car extends Vehicle {
private int speed;
public int getSpeed() {
return speed;
}
public void setSpeed(final int speed) {
this.speed = speed;
}
}
Note that I used JSON, not YAML, and you need to add your other subtypes as well.

Jakson polymorphic Enum case

I am glued with some Jackson polymorphic problem.
I work on a web JDR Character Editor personnal project. I use Springboot and try to stuck with the phylosophy. Moreover, I try to make some independent packages, because of study-case for my real work (another springboot project).
With no Jackson configuration, I have no problem for serialization of a Competence. But when I try to get back any modification on the web editor, so when Jackson make a deserialization of a Competence, problems occur with "dependance" property.
Here are my classes:
The one I try to serialize/deserialize:
public class Competence implements Composante, ComposanteTemplate {
public enum Categorie {
APPRENTI,
COMPAGNON
}
private String nom;
private String description;
private Categorie categorie;
private Chapitre chapitre;
private AttributTemplate dependance;
private List sousCompetences = new ArrayList();
public String getNom() {
return nom;
}
public void setNom(String nom) {
this.nom = nom;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Competence getTemplate() {
return this;
}
public Categorie getCategorie() {
return categorie;
}
public void setCategorie(Categorie categorie) {
this.categorie = categorie;
}
public Chapitre getChapitre() {
return chapitre;
}
public void setChapitre(Chapitre chapitre) {
this.chapitre = chapitre;
}
public AttributTemplate getDependance() {
return dependance;
}
public void setDependance(AttributTemplate dependance) {
this.dependance = dependance;
}
public List getSousCompetences() {
return sousCompetences;
}
public void setSousCompetences(List sousCompetences) {
this.sousCompetences = sousCompetences;
}
public boolean isOuverte() {
return !sousCompetences.isEmpty();
}
}
The superclass of the property I have a problem with:
public interface AttributTemplate extends ComposanteTemplate {}
The two subclasses which could be use for Competence#dependance property:
public enum Carac implements AttributTemplate, Attribut {
FORT(Type.PHYSIQUE),
AGILE(Type.PHYSIQUE),
RESISTANT(Type.PHYSIQUE),
OBSERVATEUR(Type.PHYSIQUE),
SAVANT(Type.MENTALE),
RUSE(Type.MENTALE),
TALENTUEUX(Type.MENTALE),
CHARMEUR(Type.MENTALE);
public enum Type {
PHYSIQUE,
MENTALE
}
public final Type type;
public final String nom = name().toLowerCase();
private String description;
Carac(Type type) {
this.type = type;
}
#Override
public String getNom() { return nom; }
#Override
public String getDescription() { return description; }
#Override
public Carac getTemplate() { return this; }
public void setDescription(String description) { this.description = description; }
}
public enum ArtTemplate implements AttributTemplate {
ART_GUERRIER(2, 1),
ART_ETRANGE(1, 2),
ART_GUILDIEN(1, 1);
public static final String ART_PREFIX = "ART";
public final String nom = name().toLowerCase().replace("_", " ");
public final int nbCaracsPhysiques;
public final int nbCaracsMentales;
private String description;
ArtTemplate(int nbCaracsPhysiques, int nbCaracsMentales) {
this.nbCaracsMentales = nbCaracsMentales;
this.nbCaracsPhysiques = nbCaracsPhysiques;
}
#Override
public String getNom() {
return nom;
}
#Override
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getNbCaracs() {
return nbCaracsPhysiques + nbCaracsMentales;
}
}
The result json (and then the json I send) is:
{"nom":"Comp_1489746646510","description":"ezbuixnwrclfvmgwdviubcauenzytpzzvumnohwyhpuynxaqhkjdbqygtrmbtlschthovuyoiolkauucwokkfjnaujnufshrjboykuqce","categorie":"APPRENTI","chapitre":"GUERRE","dependance":"ART_ETRANGE","ouverte":false,"sousCompetences":[]}
QUESTION:
I understand that my problem is caused by the abstract relation AttributTemplate, and then when Jackson try to deserialize, he does not know which of Carac or ArtTemplate class to use.
I try to keep unchanged Competence (Competence come from an external jar), so no annotation on this class is possible.
I've tried many of the solutions I found (Jackson 1.5: Polymorphic Type Handling, first steps ) and the only one which has worked was to define a DeserializationProblemHandler
mapper.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, JsonParser p, String msg) throws IOException {
if (instClass == AttributTemplate.class) {
String name = p.getText();
return !name.startsWith(ArtTemplate.ART_PREFIX) ? Carac.valueOf(name) : ArtTemplate.valueOf(name);
}
return super.handleMissingInstantiator(ctxt, instClass, p, msg);
}
});
But I feel bad with this solution, because I am sure there is an other beautiful one.
So is it possible to configure the mapper in order that he is able to determine which of Carac or ArtTemplate he must use to get AttributTemplate?
EDIT:
I managed to have this:
{"nom":"Comp_1489756873433","description":"kruzueemlwisibshlkotasayfkhdqkqolvhlqgsnntndkpvbmmgklqysabiakaolempmupeyiqaztdcrhwimdksgzybbdzttwnwqjxhfo","categorie":"COMPAGNON","chapitre":"GUERRE","dependance":["mova.ged.perso.inne.Carac","AGILE"],"ouverte":true,"sousCompetences":[...]}
by configuring like this the mapper
abstract class CompetenceMixIn {
private AttributTemplate dependance;
#JsonTypeInfo(use=JsonTypeInfo.Id.CLASS, include=JsonTypeInfo.As.EXISTING_PROPERTY, property="dependance")
#JsonSubTypes({ #JsonSubTypes.Type(value = Carac.class, name = "carac"), #JsonSubTypes.Type(value = ArtTemplate.class, name = "artTemplate") })
public void setDependance(AttributTemplate dependance) {
this.dependance = dependance;
}
}
ObjectMapper mapper = jsonConverter.getObjectMapper();
mapper.addMixIn(Competence.class, CompetenceMixIn.class);
As you could see, I'm still parasited with the array that wrapped dependance value. I would (...)"dependance": "AGILE", (...) not (...)"dependance":["mova.ged.perso.inne.Carac", "AGILE"], (...)
And I don't know what to change in order to have this.
i have been looking into what you are trying to do. Unfortunatelly, I believe there are issues with Enums + inheritance.
I have an alternative solution that you could be using which is to use a custom creator and ignore unknown properties. See the following example:
public class JacksonInheritance {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Competence c = new Competence();
c.desc = "desc";
c.nome = "nome";
c.template = Att1.TEST_Att1;
String test = mapper.writeValueAsString(c);
System.out.println(test);
Competence readValue = mapper.readValue(test, Competence.class);
System.out.println(readValue.template);
}
#JsonIgnoreProperties(ignoreUnknown = true)
public static class Competence {
private static final Map<String, AttributeTemplate> templates;
static {
templates = new HashMap<>();
Stream.of(Att1.values()).forEach( a -> templates.put(a.name(), a));
Stream.of(Att2.values()).forEach( a -> templates.put(a.name(), a));
}
#JsonProperty
String nome;
#JsonProperty
String desc;
#JsonIgnore
AttributeTemplate template;
#JsonProperty("template_type")
public String getTempl() {
// Here you can do whichever way uou would like to serialise your template. This will be the key
return template.toString();
}
#JsonCreator
public static Competence create(#JsonProperty("template_type") String templateType) {
Competence c = new Competence();
c.template = templates.get(templateType);
return c;
}
}
public static interface AttributeTemplate {
}
public static enum Att1 implements AttributeTemplate {
TEST_Att1;
}
public static enum Att2 implements AttributeTemplate {
TEST2_Att2;
}
}
Here I am detaching the enum logic from the jackson logic and implement my own. This does not require a custom serialisation.
I basically say that I serialise my enum as its value (you can obviously choose which ever properties you would like for this).
My output json then looks as:
{"template_type":"TEST_Att1","nome":"nome","desc":"desc"}
At the return step I now know that the information I need to construct the correct enum template type from the template_type attribute. This is what I can inject into my factory method create.
In the create I can use my statically created map to populate the correct enum into my object. We can just create this map statically since our enums are finite and static.
The beauty of this is also that the generator is only used for creation. Using #JsonIgnoreProperties(ignoreUnknown = true), we can tell jackson to not freak out by all our custom elements in the json. It will simply deserialise any fields it can detect and leave the other ones (since we are using a custom template_type for our enum resolution).
Finally, I am ignoring the actual template in my bean because jackson won't be able to construct that.
I hope that this works for you/helps you. Sorry about the delay.
Reason for not using inheritance:
There seem to be issues with enum + inheritance in jackson. Particularly jackson by default uses reflection and calls the private constructor of the enum for generation. You may be able to get creators to work in a similar way as above though.
The deserialisation expects the template. I am going of the assumption that you do NOT necessarily want to serialise all elements of the enum. This is because the enum name, in my case TEST_Att1 makes the enum unique. There is no need to serialise and send all the different attributes these enums have around. However, Deserialization with #JsonSubTypes for no value - missing property error shows that jackson requires your template field to be at least present. This is a a slight issue, because you want to use an external property for this instead (so why include a null-field as suggested in your json just to make jackson happy)
This may not be the best solution, but I think it is relatively elegant given the restrictions. I hope that helps you,
Artur

Json Deserialization in Java /w Jackson of mixed types, contained in one array

Consider the following json, getting from an public API:
anyObject : {
attributes: [
{
"name":"anyName",
"value":"anyValue"
},
{
"name":"anyName",
"value":
{
"key":"anyKey",
"label":"anyLabel"
}
}
]
}
As you can see, sometimes the value is a simple string and sometimes its an object. Is it somehow possible to deserialize those kind of json-results, to something like:
class AnyObject {
List<Attribute> attributes;
}
class Attribute {
private String key;
private String label;
}
How would I design my model to cover both cases. Is that possible ?
Despite being hard to manage as others have pointed out, you can do what you want. Add a custom deserializer to handle this situation. I rewrote your beans because I felt your Attribute class was a bit misleading. The AttributeEntry class in the object that is an entry in that "attributes" list. The ValueObject is the class that represents that "key"/"label" object. Those beans are below, but here's the custom deserializer. The idea is to check the type in the JSON, and instantiate the appropriate AttributeEntry based on its "value" type.
public class AttributeDeserializer extends JsonDeserializer<AttributeEntry> {
#Override
public AttributeEntry deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonNode root = p.readValueAsTree();
String name = root.get("name").asText();
if (root.get("value").isObject()) {
// use your object mapper here, this is just an example
ValueObject attribute = new ObjectMapper().readValue(root.get("value").asText(), ValueObject.class);
return new AttributeEntry(name, attribute);
} else if (root.get("value").isTextual()) {
String stringValue = root.get("value").asText();
return new AttributeEntry(name, stringValue);
} else {
return null; // or whatever
}
}
}
Because of this ambiguous type inconvenience, you will have to do some type checking throughout your code base.
You can then add this custom deserializer to your object mapper like so:
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addDeserializer(AttributeEntry.class, new AttributeDeserializer());
objectMapper.registerModule(simpleModule);
Here's the AttributeEntry:
public class AttributeEntry {
private String name;
private Object value;
public AttributeEntry(String name, String value) {
this.name = name;
this.value = value;
}
public AttributeEntry(String name, ValueObject attributes) {
this.name = name;
this.value = attributes;
}
/* getters/setters */
}
Here's the ValueObject:
public class ValueObject {
private String key;
private String label;
/* getters/setters */
}

Categories

Resources