How can Vaadin handle JavaBeans with non primitive attributes - java

I have a Bean that looks like this
Class Person{
private String name;
private int age
private Properties attributes
public String getName() { return name; }
public int getAge() { return age; }
public Properties getAttributes () { return attributes; }
public void setName(String name) { this.name=name; }
public void setAge(int age) { this.age=age; }
public void setAttributes (Properties attributes) { this.attributes = attributes; }
}
Trying to use Vaadin Forms to allow the editing of the Bean, i assign the Bean to the form using
form.setItemDataSource(new BeanItem<Person>(person));
The form displays correct with the attributes showing “{eye.colour=green,hair.colour=brown}”
But when trying to commit any changes, a Conversion error is thrown in regards to Properties.< Init > not having a string constructor.
How can Vaadin Forms handle Beans with non primitive types properties ?

You'll have to create a custom fieldfactory for the form by extending DefaultFieldFactory.
See https://vaadin.com/book/-/page/components.form.html (half way through the page on how to do this). This will allow you to override the default way form fields are generated. This however doesn't provide an easy way to handle custom fields and conversions like you want.
Luckily though there is a good extension, which I use, to easily create custom fields and property conversions named the custom field plugin.
https://vaadin.com/directory#addon/customfield
With this component you can easily extend existing fields and add custom conversions.
Since there aren't many examples on how to do this out there, here is an example of how the various parts of a custom FieldFactory with a custom field work together.
public class CustomFieldFactory extends DefaultFieldFactory {
public DefaultProfileTableFieldFactory() {
}
#Override
public Field createField(Container container, Object itemId, Object propertyId, Component uiContext) {
if (propertyId.equals(YOURCUSTOMPROP)) {
Select select = new Select();
select.addItem(ITEM1);
select.addItem(ITEM2);
select.addItem(ITEM3);
return new SelectCustomField(select, String.class);
} else {
return super.createField(container, itemId, propertyId, uiContext);
}
}
private static class SelectCustomField extends FieldWrapper<String> {
protected SelectCustomField(Field wrappedField, Class<? extends String> propertyType) {
super(wrappedField, propertyType);
VerticalLayout l = new VerticalLayout();
l.addComponent(wrappedField);
setCompositionRoot(l);
l.setSizeUndefined();
this.setSizeUndefined();
wrappedField.setSizeUndefined();
}
#Override
protected String parse(Object value) throws ConversionException {
// parse value from select to your model
}
#Override
protected Object format(String value) {
// format model from your model to value to be shown
}
}
}

Related

Jackson: How to edit existing property to the JSON without modifying the POJO?

I need to edit the name of "existing field" in POJO instead of adding "extra_field". Is it possible with the approach referenced link below?
Please note I do not want to use #JsonProperty annotation.
Requirement is, I have a POJO and want to use different field name every time without change in POJO. For example I have a field c_id in POJO and some times it need to write as cust_id and another time it would be my_id.
Also note I cannot change implementation of POJO as it is already used in several modules and have generic implementation.
POJO Example:
class MyPojo {
String id;
// getter and setters
}
Expected output can be the following: (name of field can be changed)
{"cust_id": "123"}
{"my_id": "123"}
Mixins
The easiest way to modify the output of Jackson without adding annotations to the original POJO is using mixins.
Just define a mixin-class with the necessary annotations and indicate to Jackson that you want to use the mixin when serializing the original object.
private static class MyPojoMixin {
#JsonProperty("cust_id")
private String id;
}
public String serializeWithMixin(MyPojo p) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(MyPojo.class, MyPojoMixin.class);
return mapper.writeValueAsString(p);
}
Custom property naming strategy
If you need to programmatically change the field-name, you might not be able to use the mixin solution. You could then use a custom PropertyNamingStrategy:
public class IdRenamingStrategy extends PropertyNamingStrategy {
private final PropertyNamingStrategy inner;
private final String newIdPropertyName;
public IdRenamingStrategy(String newIdPropertyName) {
this(PropertyNamingStrategy.LOWER_CAMEL_CASE, newIdPropertyName);
}
public IdRenamingStrategy(PropertyNamingStrategy inner, String newIdPropertyName) {
this.inner = inner;
this.newIdPropertyName = newIdPropertyName;
}
private String translate(String propertyName) {
if ("id".equals(propertyName)) {
return newIdPropertyName;
} else {
return propertyName;
}
}
#Override
public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
return inner.nameForField(config, field, translate(defaultName));
}
#Override
public String nameForGetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return inner.nameForGetterMethod(config, method, translate(defaultName));
}
#Override
public String nameForSetterMethod(MapperConfig<?> config, AnnotatedMethod method, String defaultName) {
return inner.nameForSetterMethod(config, method, translate(defaultName));
}
#Override
public String nameForConstructorParameter(MapperConfig<?> config, AnnotatedParameter ctorParam, String defaultName) {
return inner.nameForConstructorParameter(config, ctorParam, translate(defaultName));
}
}
This can be used like this:
public String serializeWithPropertyNamingStrategy(MyPojo p) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(new IdRenamingStrategy("cust_id"));
return mapper.writeValueAsString(p));
}

JAVA How can i get a method to accept a parent class and all of it's extended classes?

I apologize if this has been answered before but either i don't know the correct verbiage or my google fu is bad.
I have a TestModel class which has the getters and setters for all the tests I use. Then I have a AdditionalTestModel class that extends the TestModel with additional getters and setters for that specific type of tests.
Now I have BuildTest Class that i want to be able to pass TestModel and any extended classes of TestModel.
public static Class<?> buildTest(Class<?> test, Class<?> template)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
Class<?> testClass = test.getClass();
Method[] testMethods = testClass.getMethods();
for (Method method : testMethods) {
String name = method.getName();
if (name.startsWith("get")) {
String testMethodType = method.getReturnType().getTypeName();
// additional code removed//
}
}
If instead of Class<?> i was using TestModel it would work for any test that i pass of Class type TestModel. But i want to be able to pass the extended class to this method as well without having to write a method for each extended class. Any recommendations?
Adding information on the models in case it matters.
public class TestModel {
private String testDescription;
private String testName;
private String apiPath;
private String method;
private String expectedTest;
private Map<String, String> header = new HashMap<>();
private Object body;
private String expectedResult;
private String testCaseId;
private String testUUID;
private List testTypes;
public String getTestDescription() {
return testDescription;
}
public void setTestDescription(String testDescription) {
this.testDescription = testDescription;
}
public String getTestName() {
return testName;
}
public void setTestName(String testName) {
this.testName = testName;
}
public String getAPIPath() {
return apiPath;
}
public void setAPIPath(String apiPath) {
this.apiPath = apiPath;
}
public String getExpectedTest() {
return expectedTest;
}
public void setExpectedTest(String testName) {
this.expectedTest = testName;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public Map<String, String> getHeader() {
return header;
}
public void setHeader(Map<String, String> header) {
this.header = header;
}
public Object getBody() {
return body;
}
public void setBody(Object body) {
this.body = body;
}
public String getExpectedResult() {
return expectedResult;
}
public void setExpectedResult(String expectedResult) {
this.expectedResult = expectedResult;
}
public String getTestCaseId() {
return testCaseId;
}
public void setTestCaseId(String testCaseId) {
this.testCaseId = testCaseId;
}
public String getTestUUID() {
return testUUID;
}
public void setTestUUID(String testUUID) {
this.testUUID = testUUID;
}
public List getTestTypes() {
return testTypes;
}
public void setTestTypes(List testTypes) {
this.testTypes = testTypes;
}
}
public class AdditionalTestModel extends TestModel {
#Override public Object getBody() {
return super.getBody();
}
}
Edit: per a request adding the call information here:
#Test(dataProvider = "Default", threadPoolSize = THREADS, timeOut = API_TIME_OUT)
#Description("")
public void sampleTest(AdditionalTestModel testFromDataProvider) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
testSetup(testFromDataProvider);
AdditionalTestModel test = BuildTest.buildTest(testFromDataProvider, template);
Response response = RestAPI.call(test, testEnvironment);
if (null != response) {
ValidateAPIResponse.validateTestModel(test, response);
} else {
Assert.fail("Response is null, probably a bad method.");
}
}
Where testFromDataProvider is passed from a TestNg data provider.
Now LppEdd below already pointed out i could only assign the base class using generics so working on trying it his way, just have not gotten a chance to change things up yet.
Edit: Also realize now my question was bad. Thanks LppEdd. I should have asked How can I get a method to accept an instance of a class and an instance of any extended class
You are close, you just need to use the extends modifier.
If the class passed in as the test and template parameter should be the same exact class type, you can do:
public static <T extends TestModel> Class<T> buildTest(Class<T> test, Class<T> template) { ... }
Otherwise you can do
public static Class<? extends extends TestModel> buildTest(Class<? extends TestModel> test, Class<? extends String> extends TestModel) { ... }
Which will allow different types to be returned and passed in to each parameter.
You can read up on Java generics and wilcards starting here: https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html
Your buildTest method must accept a TestModel class.
You might be looking for something like
public static TestModel buildTest(
final TestModel test,
final TestModel template) {
final Class<? extends TestModel> testClass = test.getClass();
final Method[] testMethods = testClass.getMethods();
for (final Method method : testMethods) {
final String name = method.getName();
if (name.startsWith("get")) {
final String testMethodType = method.getReturnType().getTypeName();
// additional code removed
}
}
// Maybe
return yourNewInstance; // yourNewInstance is a TestModel, or any class extending it
}
The template argument seems unused here (clarify).
What's the wanted return type? (clarify)
Usage example
final TestModel value1 = buildTest(new TestModel(), ...);
final TestModel value2 = buildTest(new AdditionalTestModel(), ...);
This looks to be exactly the same problem as must be solved by test frameworks. For example, see junit (https://junit.org/junit5/).
The core problem is how to obtain the collection of test methods of a class.
A direct solution would be to have the test class be required to answer its test methods, say, Collection<Function<Void, Void>> getTests(); This has several problems, one being that sub-classes must explicitly list their test methods, two being that sub-classes must be careful to add in the test methods from their super-class, and third, this really fits more as static behavior, which would try to shift java instance typing to the class layer, which just isn't supported by java.
An indirect solution would be to require that test methods satisfy a particular pattern (for example, must start with "test" and have no parameters), and use reflection to discover the methods. Or, use an annotation (say, #Test, which is what junit does) to mark out test methods, and again use the java reflection API to discover methods with the marker.

Change Json key name dynamically - Jackson

I have a java class like :
class TestJsonClass {
private String propertyA;
private String propertyB;
private String propertyC;
}
Now during runtime i want to give different property names for each of the property, and not a static one using #JsonProperty("sample")
How do I accomplish this? I am using Jackson library ad Spring MVC
Thanks in advance...
You can make use of Modules for this purpose. This is the easiest solutions to your problem. Here is an example:
A simple class that can carry your property-name-mappings for each request:
public class PropertyNameMapper {
// The class for which the mappings need to take place.
public Class<?> classToFilter;
// The mappings property names. Key would be the existing property name
// value would be name you want in the ouput.
public Map<String, String> nameMappings = Collections.emptyMap();
public PropertyNameMapper(Class<?> classToFilter, Map<String, String> nameMappings) {
this.classToFilter = classToFilter;
this.nameMappings = nameMappings;
}
}
A custom BeanPropertyWriter that will be used for specifying the output name for the properties.
public class MyBeanPropertyWriter extends BeanPropertyWriter {
// We would just use the copy-constructor rather than modifying the
// protected properties. This is more in line with the current design
// of the BeanSerializerModifier class (according to its documentation).
protected MyBeanPropertyWriter(BeanPropertyWriter base, String targetName) {
super(base, new SerializedString(targetName));
}
}
Now, a custom BeanSerializerModifier that is called each time to allow you to modify the serialized properties.
public class MySerializerModifier extends BeanSerializerModifier {
public List<BeanPropertyWriter> changeProperties(
SerializationConfig config, BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
List<PropertyNameMapper> propertyMappings = getNameMappingsFromRequest();
PropertyNameMapper mapping = mappingsForClass(propertyMappings,
beanDesc.getBeanClass());
if (mapping == null) {
return beanProperties;
}
List<BeanPropertyWriter> propsToWrite = new ArrayList<BeanPropertyWriter>();
for (BeanPropertyWriter propWriter : beanProperties) {
String propName = propWriter.getName();
String outputName = mapping.nameMappings.get(propName);
if (outputName != null) {
BeanPropertyWriter modifiedWriter = new MyBeanPropertyWriter(
propWriter, outputName);
propsToWrite.add(modifiedWriter);
} else {
propsToWrite.add(propWriter);
}
}
return propsToWrite;
}
private List<PropertyNameMapper> getNameMappingsFromRequest() {
RequestAttributes requestAttribs = RequestContextHolder
.getRequestAttributes();
List<PropertyNameMapper> nameMappings = (List<PropertyNameMapper>) requestAttribs
.getAttribute("nameMappings",
RequestAttributes.SCOPE_REQUEST);
return nameMappings;
}
private PropertyNameMapper mappingsForClass(
List<PropertyNameMapper> nameMappings, Class<?> beanClass) {
for (PropertyNameMapper mapping : nameMappings) {
if (mapping.classToFilter.equals(beanClass)) {
return mapping;
}
}
return null;
}
}
Now, you need a custom Module to be able to customize the output using the above created BeanSerializerModifier:
public class MyModule extends Module {
#Override
public String getModuleName() {
return "Test Module";
}
#Override
public void setupModule(SetupContext context) {
context.addBeanSerializerModifier(new MySerializerModifier());
}
#Override
public Version version() {
// Modify if you need to.
return Version.unknownVersion();
}
}
Now register this module with your ObjectMapper. You can get the Jackson HTTP message converter from your spring application context, and get its object mapper.
// Figure out a way to get the ObjectMapper.
MappingJackson2HttpMessageConverter converter = ... // get the jackson-mapper;
converter.getObjectMapper().registerModule(new MyModule())
And that's it. This is the easiest way to customize serialization of your properties dynamically.
To use this, create a List of PropertyNameMappers and add it as an attribute (named "nameMappings" in this example) in the current request.
This is an example, not production-ready code. You might probably need to add null-checks and things like that. Also, a few minor adjustments might be needed based on the version of the libraries you are using.
If the solution doesn't work for you, let me know the problems you are facing.
You could inject a custom PropertyNamingStrategy into the ObjectMapper that's used in deserialization.
Just set fields into the PropertyNamingStrategy at runtime, assuming you can map them to something like the default JsonPropertyName (e.g. propertyA, propertyB, propertyC).
public class MyNamingStrategy extends PropertyNamingStrategy {
String propertyAName, propertyBName, propertyCName;
public MyNamingStrategy(String propANm, String propBNm, String propCNm) {
this.propertyAName = propANm;
//finish
}
#Override
public String nameForField(MapperConfig<?> config, AnnotatedField field,
String defaultName) {
return convert(defaultName);
}
#Override
public String nameForGetterMethod(MapperConfig<?> config,
AnnotatedMethod method, String defaultName) {
return convert(defaultName);
}
#Override
public String nameForSetterMethod(MapperConfig<?> config,
AnnotatedMethod method, String defaultName) {
return convert(defaultName);
}
public String convert(String defaultName ){
return defaultName.replace("propertyA", propertyAName).replace( //finish
}
Finally you'd create an instance and inject it at runtime.
objectMapper.setNamingStrategy(myNamingStrategyInstance));
See this Cowtowncoder post for more on PropertyNamingStrategy:
Jackson 1.8: custom property naming strategies
Or this documentation:
github.com/FasterXML/jackson-docs/wiki/PropertyNamingStrategy

Jackson - custom serializer that overrides only specific fields

I know how to use a custom serializer in Jackson (by extending JsonSerializer), but I want the default serializer to work for all fields, except for just 1 field, which I want to override using the custom serializer.
Annotations are not an option, because I am serializing a generated class (from Thrift).
How do I specify only certain fields to be overridden when writing a custom jackson serializer?
Update:
Here's the class I want to serialize:
class Student {
int age;
String firstName;
String lastName;
double average;
int numSubjects
// .. more such properties ...
}
The above class has many properies, most of which use native types. I want to just override a few properties in the custom serializer and let Jackson deal with the rest as usual. For e.g. I just want to convert the "age" field to a custom output.
Assuming your Target class is
public class Student {
int age;
String firstName;
String lastName;
double average;
int numSubjects;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public double getAverage() {
return average;
}
public void setAverage(double average) {
this.average = average;
}
public int getNumSubjects() {
return numSubjects;
}
public void setNumSubjects(int numSubjects) {
this.numSubjects = numSubjects;
}
}
You need to write a custom serializer as given below
public class MyCustomSerializer extends JsonSerializer<Student> {
#Override
public void serialize(Student value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
if (value != null) {
jgen.writeStartObject();
jgen.writeStringField("age", "Age: " + value.getAge()); //Here a custom way to render age field is used
jgen.writeStringField("firstName", value.getFirstName());
jgen.writeStringField("lastName", value.getLastName());
jgen.writeNumberField("average", value.getAverage());
jgen.writeNumberField("numSubjects", value.getNumSubjects());
//Write other properties
jgen.writeEndObject();
}
}
}
then add it to the ObjectMapper
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("custom",
Version.unknownVersion());
module.addSerializer(Student.class, new MyCustomSerializer());
mapper.registerModule(module);
then use it like
Student s = new Student();
s.setAge(2);
s.setAverage(3.4);
s.setFirstName("first");
s.setLastName("last");
s.setNumSubjects(3);
StringWriter sw = new StringWriter();
mapper.writeValue(sw, s);
System.out.println(sw.toString());
It will produce a o/p like
{"age":"Age:
2","firstName":"first","lastName":"last","average":3.4,"numSubjects":3}
Just because you can not modify classes DOES NOT mean you could not use annotations: just use mix-in annotations. See this blog entry for example (or google for more with "jackson mixin annotations") for how to use this.
I have specifically used Jackson with protobuf- and thrift-generated classes, and they work pretty well. For earlier Thrift versions, I had to disable discovery of "is-setters", methods Thrift generates to see if a specific property has been explicitly set, but otherwise things worked fine.
I faced the same issue, and I solved it with CustomSerializerFactory.
This approach allows you to ignore some specific field for either for all objects, or for specific types.
public class EntityCustomSerializationFactory extends CustomSerializerFactory {
//ignored fields
private static final Set<String> IGNORED_FIELDS = new HashSet<String>(
Arrays.asList(
"class",
"value",
"some"
)
);
public EntityCustomSerializationFactory() {
super();
}
public EntityCustomSerializationFactory(Config config) {
super(config);
}
#Override
protected void processViews(SerializationConfig config, BeanSerializerBuilder builder) {
super.processViews(config, builder);
//ignore fields only for concrete class
//note, that you can avoid or change this check
if (builder.getBeanDescription().getBeanClass().equals(Entity.class)){
//get original writer
List<BeanPropertyWriter> originalWriters = builder.getProperties();
//create actual writers
List<BeanPropertyWriter> writers = new ArrayList<BeanPropertyWriter>();
for (BeanPropertyWriter writer: originalWriters){
String propName = writer.getName();
//if it isn't ignored field, add to actual writers list
if (!IGNORED_FIELDS.contains(propName)){
writers.add(writer);
}
}
builder.setProperties(writers);
}
}
}
And afterwards you can use it something like the following:
objectMapper.setSerializerFactory(new EntityCustomSerializationFactory());
objectMapper.writeValueAsString(new Entity());//response will be without ignored fields
In case you don't want to pollute your model with annotations, you could use mixins.
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.setMixInAnnotation(Student.class, StudentMixin.class);
mapper.registerModule(simpleModule);
And you want to override id field for example:
public abstract class StudentMixin {
#JsonSerialize(using = StudentIdSerializer.class)
public String id;
}
Do whatever you need with the field:
public class StudentIdSerializer extends JsonSerializer<Integer> {
#Override
public void serialize(Integer integer, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(String.valueOf(integer * 2));
}
}
with the help of #JsonView we can decide fields of model classes to serialize which satisfy the minimal criteria ( we have to define the criteria) like we can have one core class with 10 properties but only 5 properties can be serialize which are needful for client only
Define our Views by simply creating following class:
public class Views
{
static class Android{};
static class IOS{};
static class Web{};
}
Annotated model class with views:
public class Demo
{
public Demo()
{
}
#JsonView(Views.IOS.class)
private String iosField;
#JsonView(Views.Android.class)
private String androidField;
#JsonView(Views.Web.class)
private String webField;
// getters/setters
...
..
}
Now we have to write custom json converter by simply extending HttpMessageConverter class from spring as:
public class CustomJacksonConverter implements HttpMessageConverter<Object>
{
public CustomJacksonConverter()
{
super();
//this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.ClientView.class));
this.delegate.getObjectMapper().configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
this.delegate.getObjectMapper().setSerializationInclusion(Include.NON_NULL);
}
// a real message converter that will respond to methods and do the actual work
private MappingJackson2HttpMessageConverter delegate = new MappingJackson2HttpMessageConverter();
#Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return delegate.canRead(clazz, mediaType);
}
#Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return delegate.canWrite(clazz, mediaType);
}
#Override
public List<MediaType> getSupportedMediaTypes() {
return delegate.getSupportedMediaTypes();
}
#Override
public Object read(Class<? extends Object> clazz,
HttpInputMessage inputMessage) throws IOException,
HttpMessageNotReadableException {
return delegate.read(clazz, inputMessage);
}
#Override
public void write(Object obj, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException
{
synchronized(this)
{
String userAgent = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("userAgent");
if ( userAgent != null )
{
switch (userAgent)
{
case "IOS" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.IOS.class));
break;
case "Android" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView(Views.Android.class));
break;
case "Web" :
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( Views.Web.class));
break;
default:
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( null ));
break;
}
}
else
{
// reset to default view
this.delegate.getObjectMapper().setConfig(this.delegate.getObjectMapper().getSerializationConfig().withView( null ));
}
delegate.write(obj, contentType, outputMessage);
}
}
}
Now there is need to tell spring to use this custom json convert by simply putting this in dispatcher-servlet.xml
<mvc:annotation-driven>
<mvc:message-converters register-defaults="true">
<bean id="jsonConverter" class="com.mactores.org.CustomJacksonConverter" >
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
That's how you will able to decide which fields to get serialize.
Thanx

Jackson Mapper post-construct

I am using the Jackson ObjectMapper to deserialize some JSON into a Java class, which we'll call PlayerData. I would like to add a bit of logic to the PlayerData class to fix up some data after the fields have been loaded in. For example, some early JSON files used to use a "sex" flag instead of a "gender" falg, so if the sex flag is set but the gender flag is not set, I'd like to set the value of the gender field to be the value of the sex field.
Is there some sort of #PostConstruct or #AfterLoad annotation that I could affix to a method? Or perhaps an interface that I could implement? I didn't notice one in the documentation, but it seemed like an obvious feature.
Found this thru a link in the comments (credit: fedor.belov). This appears to allow you to run code post construct.
Adding a comment for people who end up here via
http://jira.codehaus.org/browse/JACKSON-645 or
http://jira.codehaus.org/browse/JACKSON-538 and are looking for a
method which is called after a deserializer completes. I was able to
achieve the desired effect by including an annotation and writing a
converter which uses the same class as input and output.
#JsonDeserialize(converter=MyClassSanitizer.class) // invoked after class is fully deserialized
public class MyClass {
public String field1;
}
import com.fasterxml.jackson.databind.util.StdConverter;
public class MyClassSanitizer extends StdConverter<MyClass,MyClass> {
#Override
public MyClass convert(MyClass var1) {
var1.field1 = munge(var1.field1);
return var1;
}
}
If you're not using the #JsonCreator, then Jackson will use the setter and getter methods to set the fields.
So if you define the following methods assuming that you have Sex and Gender enums:
#JsonProperty("sex")
public void setSex(final Sex sex) {
this.sex = sex;
if (gender == null) {
gender = (sex == Sex.WOMAN) ? Gender.WOMAN : Gender.MAN;
}
}
#JsonProperty("gender")
public void setGender(final Gender gender) {
this.gender = gender;
if (sex == null) {
sex = (gender == Gender.WOMAN) ? Sex.WOMAN : Sex.MAN;
}
}
it would work.
Update: You can find all of the annotations of Jackson library here.
Update2: Other solution:
class Example {
private final Sex sex;
private final Gender gender;
#JsonCreator
public Example(#JsonProperty("sex") final Sex sex) {
super();
this.sex = sex;
this.gender = getGenderBySex(sex)
}
#JsonFactory
public static Example createExample(#JsonProperty("gender") final Gender gender) {
return new Example(getSexByGender(gender));
}
private static Sex getSexByGender(final Gender) {
return (gender == Gender.WOMAN) ? Sex.WOMAN : Sex.MAN;
}
private static Gender getGenderBySex(final Sex) {
return (sex == Sex.WOMAN) ? Gender.WOMAN : Gender.MAN;
}
}
This is not supported out of the box, but you can easily create your #JsonPostDeserialize annotation for methods to be called after deserialization.
First, define the annotation:
/**
* Annotation for methods to be called directly after deserialization of the object.
*/
#Target({ ElementType.METHOD })
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonPostDeserialize {
}
Then, add the following registration and implementation code to your project:
public static void addPostDeserializeSupport(ObjectMapper objectMapper) {
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDescription,
JsonDeserializer<?> originalDeserializer) {
return new CustomAnnotationsDeserializer(originalDeserializer, beanDescription);
}
});
objectMapper.registerModule(module);
}
/**
* Class implementing the functionality of the {#link JsonPostDeserialize} annotation.
*/
public class CustomAnnotationsDeserializer extends DelegatingDeserializer {
private final BeanDescription beanDescription;
public CustomAnnotationsDeserializer(JsonDeserializer<?> delegate, BeanDescription beanDescription) {
super(delegate);
this.beanDescription = beanDescription;
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new CustomAnnotationsDeserializer(newDelegatee, beanDescription);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
Object deserializedObject = super.deserialize(p, ctxt);
callPostDeserializeMethods(deserializedObject);
return deserializedObject;
}
private void callPostDeserializeMethods(Object deserializedObject) {
for (AnnotatedMethod method : beanDescription.getClassInfo().memberMethods()) {
if (method.hasAnnotation(JsonPostDeserialize.class)) {
try {
method.callOn(deserializedObject);
} catch (Exception e) {
throw new RuntimeException("Failed to call #JsonPostDeserialize annotated method in class "
+ beanDescription.getClassInfo().getName(), e);
}
}
}
}
}
Finally, modify your ObjectMapper instance with addPostDeserializeSupport, it will invoke all #JsonPostDeserialize annotated method of deserialized objects.
This is something that has actually been suggested couple of times earlier. So maybe filing an RFE would make sense; there are multiple ways in which this could work: obvious ones being ability to annotate type (#JsonPostProcess(Processor.class)) and ability to register post-processor through Module API (so that there's basically a callback when Jackson constructs deserializer, to let module specify post-processor to use if any). But perhaps there are even better ways to do this.

Categories