I'm trying to create custom jackson annotation that would affect serialization value.
Meaning:
class X {
#Unit("mm") int lenght;
...
}
Now serializaling object X(10) would result in:
{
"lenght" : "10 mm"
}
How can I achieve that?
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// Create own annotation storing your unit value
#Target({ ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
#interface Unit {
String value();
}
// Create custom serializer checking #Unit annotation
class UnitSerializer extends StdSerializer<Integer> implements ContextualSerializer {
private String unit;
public UnitSerializer() {
super(Integer.class);
}
public UnitSerializer(String unit) {
super(Integer.class);
this.unit = unit;
}
#Override
public void serialize(Integer value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(String.format("%d %s", value, unit));
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
String unit = null;
Unit ann = null;
if (property != null) {
ann = property.getAnnotation(Unit.class);
}
if (ann != null) {
unit = ann.value();
}
return new UnitSerializer(unit);
}
}
#NoArgsConstructor
#AllArgsConstructor
#Data
class X {
#JsonSerialize(using = UnitSerializer.class)
#Unit("mm")
private int length;
}
public class Runner {
public static void main(String[] args) throws JsonProcessingException {
X x = new X(10);
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(x));
}
}
Result:
{"length":"10 mm"}
Related
Is there a way to skip some properties on deserialization but at the same time knowing are they presented or not?
{
"id": 123,
"name": "My Name",
"picture": {
// a lot of properties that's not important for me
}
}
#JsonIgnoreProperties(ignoreUnknown=true)
#JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private int id;
}
So, I ignoreUnknown is what I want as a default behavior because I don't want name field and all other fields that can exist. The value of picture fields also is not important. I just want to know was picture property available or not. How I can do that?
You can add a boolean property and custom deserializer which just reads given value and returns true. Jackson invokes custom deserializer only if property exists in payload.
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.File;
import java.io.IOException;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./src/main/resources/test.json");
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(jsonFile, User.class));
}
}
class PropertyExistsJsonDeserializer extends JsonDeserializer<Boolean> {
#Override
public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
p.readValueAsTree(); //consume value
return Boolean.TRUE;
}
}
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(JsonInclude.Include.NON_NULL)
class User {
private int id;
#JsonDeserialize(using = PropertyExistsJsonDeserializer.class)
#JsonProperty("picture")
private boolean pictureAvailable;
//getters, setters, toString
}
Above code prints:
User{id=123, pictureAvailable=true}
My goal is code generation via annotation processor. I want to generate new class on the top of the existent base class by excluding some fields according to annotations and adding some constarint validators etc. I have 3 modules. First one is base module which contains Car class and annotations BaseClass, A and B. Second module is annotation processor module. It contains CustomCodeGenerator annotaion and its processor. And third module is the module which I want to generate NewCar class onto it and use that NewCar class in it.
Car.Class
#BaseClass
public class Car {
#A
private int seatCount;
#B
private String name;
private String dummy;
public Car(int seatCount, String name) {
this.seatCount = seatCount;
this.name = name;
}
public int getSeatCount() {
return seatCount;
}
public void setSeatCount(int seatCount) {
this.seatCount = seatCount;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDummy() {
return dummy;
}
public void setDummy(String dummy) {
this.dummy = dummy;
}
}
CustomCodeGenerator.class
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
#Retention(RetentionPolicy.SOURCE)
#Target(ElementType.TYPE)
public #interface CustomCodeGenerator {
}
CustomCodeGeneratorProcessor.class
import com.squareup.javapoet.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.io.IOException;
import java.util.List;
import java.util.Set;
#SupportedAnnotationTypes("*")
public class CustomCodeGeneratorProcessor extends AbstractProcessor {
private Filer filer;
private Messager messager;
private Elements elementUtils;
private Types typeUtils;
#Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
messager = processingEnvironment.getMessager();
elementUtils = processingEnvironment.getElementUtils();
typeUtils = processingEnvironment.getTypeUtils();
}
#Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!roundEnv.processingOver()) {
try {
Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(BaseClass.class);
for (Element element : elementsAnnotatedWith) {
TypeElement element1 = (TypeElement) element;
List<? extends Element> enclosedElements = element1.getEnclosedElements();
MethodSpec main = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(Integer.class, "seatCount")
.addStatement("this.$N = $N", "seatCount", "seatCount")
.build();
TypeSpec.Builder builder = TypeSpec.classBuilder("NewCar")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(AnnotationSpec.builder(ClassName.get("", "ValidPassengerCount")).build())
.addMethod(main);
outer:
for (Element enclosedElement : enclosedElements) {
if (enclosedElement.getKind().isField()) {
List<? extends AnnotationMirror> annotationMirrors = enclosedElement.getAnnotationMirrors();
for (AnnotationMirror declaredAnnotation : annotationMirrors) {
if (!typeUtils.isSameType(elementUtils.getTypeElement("A").asType(), declaredAnnotation.getAnnotationType())) {
continue outer;
}
}
builder.addField(TypeName.get(enclosedElement.asType()), enclosedElement.getSimpleName().toString(), Modifier.PUBLIC);
}
}
JavaFile javaFile = JavaFile.builder("", builder.build())
.build();
javaFile.writeTo(filer);
}
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
#Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
The third module is like below as well.
Main.class
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
#CustomCodeGenerator
public class Main {
public static void main(String[] args) {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
NewCar car = new NewCar(-1);
Set<ConstraintViolation<NewCar>> violationSet = validator.validate(car);
System.out.println(violationSet.iterator().next().getMessage());
}
}
ValidPassengerCount.class
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
#Target({TYPE, ANNOTATION_TYPE})
#Retention(RUNTIME)
#Constraint(validatedBy = {ValidPassengerCountValidator.class})
public #interface ValidPassengerCount {
String message() default "invalid passenger count!";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
ValidPassengerCountValidator.class
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class ValidPassengerCountValidator
implements ConstraintValidator<ValidPassengerCount, NewCar> {
public void initialize(ValidPassengerCount constraintAnnotation) {
}
public boolean isValid(NewCar car, ConstraintValidatorContext context) {
if (car == null) {
return true;
}
return 0 <= car.seatCount;
}
}
The problem is roundEnv.getElementsAnnotatedWith(BaseClass.class) in CustomCodeGeneratorProcessor.class returns empty list. If I move Car into the 3rd module it works. However my goal is generating new code from the base class which comes from dependent module which is module 1 for this example. Is there any way to reach annotated elements of dependent module?
I would like to create a custom annotation in my Spring Boot application which always adds a prefix to my class level RequestMapping path.
My Controller:
import com.sagemcom.smartvillage.smartvision.common.MyApi;
import org.springframework.web.bind.annotation.GetMapping;
#MyApi("/users")
public class UserController {
#GetMapping("/stackoverflow")
public String get() {
return "Best users";
}
}
My custom annotation
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#RestController
#RequestMapping(path = "/api")
public #interface MyApi {
#AliasFor(annotation = RequestMapping.class)
String value();
}
GOAL: a mapping like this in the end: /api/users/stackoverflow
Notes:
server.servlet.context-path is not an option because I want to create
several of these
I'm using Spring Boot version 2.0.4
I was not able to find an elegant solution for the issue. However, this worked:
Slightly modified annotation, because altering behavior of value turned out to be more difficult.
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Documented
#RestController
#RequestMapping
public #interface MyApi {
#AliasFor(annotation = RequestMapping.class, attribute = "path")
String apiPath();
}
Bean Annotation Processor
import com.sagemcom.smartvillage.smartvision.common.MyApi;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
#Component
public class MyApiProcessor implements BeanPostProcessor {
private static final String ANNOTATIONS = "annotations";
private static final String ANNOTATION_DATA = "annotationData";
public Object postProcessBeforeInitialization(#NonNull final Object bean, String beanName) throws BeansException {
MyApi myApi = bean.getClass().getAnnotation(MyApi.class);
if (myApi != null) {
MyApi alteredMyApi = new MyApi() {
#Override
public Class<? extends Annotation> annotationType() {
return MyApi.class;
}
#Override
public String apiPath() {
return "/api" + myApi.apiPath();
}
};
alterAnnotationOn(bean.getClass(), MyApi.class, alteredMyApi);
}
return bean;
}
#Override
public Object postProcessAfterInitialization(#NonNull Object bean, String beanName) throws BeansException {
return bean;
}
#SuppressWarnings("unchecked")
private static void alterAnnotationOn(Class clazzToLookFor, Class<? extends Annotation> annotationToAlter, Annotation annotationValue) {
try {
// In JDK8 Class has a private method called annotationData().
// We first need to invoke it to obtain a reference to AnnotationData class which is a private class
Method method = Class.class.getDeclaredMethod(ANNOTATION_DATA, null);
method.setAccessible(true);
// Since AnnotationData is a private class we cannot create a direct reference to it. We will have to manage with just Object
Object annotationData = method.invoke(clazzToLookFor);
// We now look for the map called "annotations" within AnnotationData object.
Field annotations = annotationData.getClass().getDeclaredField(ANNOTATIONS);
annotations.setAccessible(true);
Map<Class<? extends Annotation>, Annotation> map = (Map<Class<? extends Annotation>, Annotation>) annotations.get(annotationData);
map.put(annotationToAlter, annotationValue);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Controller:
import com.sagemcom.smartvillage.smartvision.common.MyApi;
import org.springframework.web.bind.annotation.GetMapping;
#MyApi(apiPath = "/users")
public class UserController {
#GetMapping("/stackoverflow")
public String get() {
return "Best users";
}
}
Same #JsonIgnore applied to both String and List. It is working on String but not List.
Jackson version 2.3.0. Below is the sample program to print out the output for setter properties. Setter properties still shown for testList property.
The workaround might be mapper.disable(MapperFeature.USE_GETTERS_AS_SETTERS); but it is not my desired behaviour. I want to control on the object itself.
package my.com.myriadeas.ilmu.bootstrap;
import java.util.Iterator;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.TypeFactory;
public class JsonFactoryTest {
/**
* #param args
* #throws JsonMappingException
*/
public static void main(String[] args) throws JsonMappingException {
ObjectMapper mapper = new ObjectMapper();
//mapper.disable(MapperFeature.USE_GETTERS_AS_SETTERS);
mapper.registerModule(new TestJacksonModule());
JavaType javaType = TypeFactory.defaultInstance().uncheckedSimpleType(
TestJsonProperty.class);
System.out.println(mapper.canDeserialize(javaType));
}
public static class TestJsonProperty {
#JsonIgnore
private String testString;
#JsonProperty
public String getTestString() {
return testString;
}
#JsonIgnore
public void setTestString(String testString) {
this.testString = testString;
}
#JsonIgnore
private List<String> testList;
#JsonProperty
public List<String> getTestList() {
return testList;
}
#JsonIgnore
public void setTestList(List<String> testList) {
this.testList = testList;
}
}
public static class TestJacksonModule extends SimpleModule {
/**
*
*/
private static final long serialVersionUID = -8628204972239032380L;
public TestJacksonModule() {
setDeserializerModifier(new AssociationUriResolvingDeserializerModifier());
}
}
private static class AssociationUriResolvingDeserializerModifier extends
BeanDeserializerModifier {
public AssociationUriResolvingDeserializerModifier() {
}
#Override
public BeanDeserializerBuilder updateBuilder(
DeserializationConfig config, BeanDescription beanDesc,
BeanDeserializerBuilder builder) {
Iterator<SettableBeanProperty> properties = builder.getProperties();
while (properties.hasNext()) {
System.out.println("deserialize property= " + properties.next());
}
return builder;
}
}
}
in my app I have controller:
package org.sample.web.config;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.sample.model.Contact;
import org.sample.model.Person;
import org.sample.model.PersonGender;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import ch.ralscha.extdirectspring.generator.IncludeValidation;
import ch.ralscha.extdirectspring.generator.ModelGenerator;
import ch.ralscha.extdirectspring.generator.OutputFormat;
#Controller
#RequestMapping
public class ModelController {
private final Logger logger = LoggerFactory.getLogger(getClass());
#InitBinder
public void initBinder(WebDataBinder binder) {
logger.error("aaaaaaaaaaaaaaa");
binder.registerCustomEditor(PersonGender.class, new EnumPropertyEditor(PersonGender.class));
}
#RequestMapping("/app/model/Person.js")
public void user(HttpServletRequest request, HttpServletResponse response) throws IOException {
ModelGenerator.writeModel(request, response, Person.class, OutputFormat.EXTJS4, IncludeValidation.ALL, true);
}
#RequestMapping("/app/model/Contact.js")
public void catalog(HttpServletRequest request, HttpServletResponse response) throws IOException {
ModelGenerator.writeModel(request, response, Contact.class, OutputFormat.EXTJS4, IncludeValidation.ALL, true);
}
}
problem is that method with annotation InitBinder is never calling so when I want to validate my enut it throws exception:
javax.validation.UnexpectedTypeException: HV000030: No validator could be found for type: org.sample.model.PersonGender.
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager.verifyResolveWasUnique(ConstraintValidatorManager.java:218)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager.findMatchingValidatorClass(ConstraintValidatorManager.java:193)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintValidatorManager.getInitializedValidator(ConstraintValidatorManager.java:97)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:125)
at org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree.validateConstraints(ConstraintTree.java:91)
at org.hibernate.validator.internal.metadata.core.MetaConstraint.validateConstraint(MetaConstraint.java:85)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraint(ValidatorImpl.java:478)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForDefaultGroup(ValidatorImpl.java:424)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateConstraintsForCurrentGroup(ValidatorImpl.java:388)
at org.hibernate.validator.internal.engine.ValidatorImpl.validateInContext(ValidatorImpl.java:340)
at org.hibernate.validator.internal.engine.ValidatorImpl.validate(ValidatorImpl.java:158)
at org.springframework.validation.beanvalidation.SpringValidatorAdapter.validate(SpringValidatorAdapter.java:102)
at org.springframework.validation.DataBinder.validate(DataBinder.java:772)
EDIT:
PersonGender:
package org.sample.model;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
public enum PersonGender implements Serializable {
MALE("M"), FEMALE("F"), UNKNOWN("0");
private static final long serialVersionUID = 1L;
private static Map<String, PersonGender> identifierMap = new HashMap<String, PersonGender>();
static {
for (PersonGender value : PersonGender.values()) {
identifierMap.put(value.getValue(), value);
}
}
private String value;
private PersonGender(String value) {
this.value = value;
}
public static PersonGender fromValue(String value) {
PersonGender result = identifierMap.get(value);
if (result == null) {
throw new IllegalArgumentException("No PersonGender for value: " + value);
}
return result;
}
public String getValue() {
return value;
}
public String getName() {
return name();
}
}
EXCEPTIon occurs when submiting form:
#Service
public class SimpleService {
...
#ExtDirectMethod(value = ExtDirectMethodType.FORM_POST, group = "person")
public ExtDirectFormPostResult validatePersonForm(#Valid Person p, BindingResult result) {
if (!result.hasErrors()) {
// another validations
}
return new ExtDirectFormPostResult(result);
}
...
}