I would like to create a customized Jackson annotation like that:
#JacksonAnnotationsInside
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.FIELD, ElementType.TYPE})
//... Other Jackson annotations...
#interface SelfLink {
Class<? extends Entity> type();
String uriTemplate() default "";
}
I would like to use this annotation on classes or fields like this:
#Getter
#SelfLink(type = Alpha.class)
class Alpha extends Entity {
private String name;
public Alpha(Long id, String name) {
super(id);
this.name = name;
}
}
#Getter
class Beta {
private String uuid;
#SelfLink(type = Gamma.class)
private Entity data;
}
#Getter
class Gamma extends Entity {
private String stuff;
public Gamma(Long id, String stuff) {
super(id);
this.stuff = stuff;
}
}
The customized Jackson annotation would be used to append extra fields. I tried to use #JsonSerialize(using = SelfLinkSerializer.class) on the SelfLink annotation to define a Serializer:
#JsonComponent
class SelfLinkSerializer extends StdSerializer<Object> {
private Gson gson = new Gson();
private Mirror mirror = new Mirror();
#Autowired
private LinkResolver linkResolver;
#SuppressWarnings("unused")
private SelfLinkSerializer() {
this(Object.class,
new LinkResolver() {
#Override
public <T extends Entity> String resolve(Class<? extends Entity> type, T instance) {
return "always-null";
}
}
);
}
SelfLinkSerializer(Class<Object> t, LinkResolver linkResolver) {
super(t);
this.linkResolver = linkResolver;
}
#Autowired
public SelfLinkSerializer(LinkResolver linkResolver) {
this(Object.class, linkResolver);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
this.serializeContentWithoutThisSerializer(value, gen, provider);
gen.writeEndObject();
}
private void serializeContentWithoutThisSerializer(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
String jsonContent = this.serializeContentViaGson(value, gen, provider);
gen.writeRaw(jsonContent);
}
private String serializeContentViaGson(Object value, JsonGenerator gen, SerializerProvider provider) {
JsonElement jsonElement = this.gson.toJsonTree(value);
SelfLink selfLink = this.mirror.on(value.getClass()).reflect().annotation(SelfLink.class).atClass();
if(selfLink != null) {
Class<? extends Entity> type = selfLink.type();
String link = value instanceof Entity ? this.linkResolver.resolve(type, (Entity) value) : null;
jsonElement.getAsJsonObject().addProperty("link", link);
}
String json = this.gson.toJson(jsonElement);
String trimmed = CharMatcher.is('{').trimFrom(json);
trimmed = CharMatcher.is('}').trimTrailingFrom(trimmed);
return trimmed;
}
}
The original idea was to start the JSON ("{"), use Jackson engine to generate the content, my serializer would resolve the link, append it on the JSON ("link":"..."), and close the JSON ("}"). But I hit a wall: I did not find a way reuse Jackson. My workaround so far is to use Gson, but that is a monstrosity. Gson do not honor Jackson annotations. I would need to create an algorithm to convert all Jackson annotations and that is a "NO-NO".
I am kinda locked on some versions of Spring Boot and Jackson (1.5.8.RELEASE and 2.8.10 respectively). I created a Gist with the whole example, and a pom.xml.
I have a secondary issue because even as a #JsonComponent, Spring is not injecting the #Autowired LinkResolver linkResolver. For now I also do not know how to make #SelfLink work on fields like on Beta class. Bur for now my question is:
How is it possible to use the Jackson API to generate the string with the JSON representation for the value param in the public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException method?
Related
I have a field in a POJO which needs to be encrypted before being serialized and, similarly, decrypted upon deserialization.
The issue is that a cryptor is a Spring bean, so I need to access Spring context in my custom serializers/deserializers.
That's how I do that now:
private static final Cryptor cryptor = ApplicationContextUtils.getApplicationContext().getBean(Cryptor.class);
I wonder though if it's possible to autowire cryptor without accessing the context manually.
Converting the serializer/deserializer to Spring beans doesn't help, as Jackson creates an instance of a serializer/deserializer using a no-args constructor, so an autowired field cryptor remains null.
A bit of code to illustrate what I'm talking about:
public class POJO {
#JsonSerialize(using = EncryptSerializer.class)
#JsonDeserialize(using = DecryptDeserializer.class)
private String code;
}
public class EncryptSerializer extends JsonSerializer<String> {
private static final Cryptor cryptor = ApplicationContextUtils.getApplicationContext().getBean(Cryptor.class);
#Override
public void serialize(String value, JsonGenerator generator, SerializerProvider serializers) throws IOException {
if (value != null) {
generator.writeString(cryptor.encrypt(value));
}
}
}
public class DecryptDeserializer extends JsonDeserializer<String> {
private static final Cryptor cryptor = ApplicationContextUtils.getApplicationContext().getBean(Cryptor.class);
#Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
String value = jsonParser.getValueAsString();
return (value != null) ? cryptor.decrypt(value) : null;
}
}
Thanks in advance.
Yes, it can. Just use #JsonComponent annotation.
I can recommend u to see baeldung article about this theme: https://www.baeldung.com/spring-boot-jsoncomponent
UPD:
here javadoc for #JsonComponent annotation: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/jackson/JsonComponent.html
I have created my request POJO as follows
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Notification {
#NotNull
private String clientId;
private String userId;
#NotNull
private String requestingService;
#NotNull
private String message;
#NotNull
private String messageType;
when I send request body as follow, it is working fine.
{
"clientId":"9563",
"userId":"5855541",
"requestingService":"cm-dm-service",
"message":"Document Created",
"messageType":"user-msg"
}
But when I sent like below
{
"clientId":"9563",
"userId":true,
"requestingService":"cm-dm-service",
"message":"Document Created",
"messageType":"user-msg"
}
Here is my controller
public ResponseEntity<Status> createNotification(#RequestBody #Valid Notification notification,
BindingResult bindingResult, HttpServletRequest request) throws AppException {
Expected: throw some error
Actual: converting true value for userId to string by jackson.
please let me know is there a way to acheive the Expected behaviour
The jackson NumberDeserializers.BooleanDeserializer is programmed to convert boolean to String.
We can override the deserializer with ours and prevent the conversion and throw an exception instead.
I can give you an example, you try to implement it to your problem statement.
Create a boolean deserialize class
public class MyDeser extends JsonDeserializer {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonToken t = p.getCurrentToken();
if (t.isBoolean()) {
throw new Exception();
}
else if (t.isNumeric()) {
throw new Exception();
}
else if (t == JsonToken.VALUE_STRING) {
return p.getValueAsString();
}
return null;
}
}
Now inject the deserializer to our application
#SpringBootApplication
#Configuration
public class Application {
#Bean
public SimpleModule injectDeser() {
return new SimpleModule().addDeserializer(String.class, new MyDeser());
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
By default, com.fasterxml.jackson.databind.deser.std.StringDeserializer accepts scalar values. You can implement custom deserialiser and throw exception in this case:
class StrictStringDeserializer extends StringDeserializer {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonToken token = p.currentToken();
if (token.isScalarValue()) {
ctxt.reportInputMismatch(String.class, "%s is not a `String` value!", token.toString());
return null;
}
return super.deserialize(p, ctxt);
}
}
To register it use SimpleModule:
SimpleModule strictModule = new SimpleModule();
strictModule.addDeserializer(String.class, new StrictStringDeserializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(strictModule);
To how to do that in Spring read: Customize the Jackson ObjectMapper.
If you only want to change it for a one field: userId use JsonDeserialize annotation:
#JsonDeserialize(using = StrictStringDeserializer.class)
private String userId;
See also:
Is it possible to use a custom serializer/deserializer for a type by
default in spring?
I want to add a wrapper which named is determined at runtime, because it depends of the class name (I could use #JsonRootName but I don't want to because I would have to use it on every sub class, which is not efficient).
I suppose I should use #JsonSerialize to override the default serializer, but I want that just to create the wrapper; I don't want to serialize the object fields myself (also I am in an abstract class, so I don't even know the fields of the sub class!). I don't care about them, I just care about the wrapper! So I would like the default serializer to handle those fields for me, or something like that.
#JsonSerialize(using = CustomSerializer.class)
public abstract class Request {
public static class CustomSerializer extends JsonSerializer<Request > {
#Override
public void serialize(Request request, JsonGenerator jgen, SerializerProvider provider) throws IOException {
// Doing my stuff to determine the wrapper name based on request.class.getSimpleName()
// Then what should I wright to serialize the fields?
// Basically I just want a function to generate the same json that the default serializer would generate!
// I tried the following, but obviously it gives a com.fasterxml.jackson.databind.JsonMappingException: Infinite recursion
jgen.writeObject(value);
// Same error for the function below
provider.defaultSerializeValue(value, jgen);
}
}
To create wrapper serialiser you need to use com.fasterxml.jackson.databind.ser.BeanSerializerModifier class. You can register it using com.fasterxml.jackson.databind.module.SimpleModule. Below example shows end-to-end solution how to do that:
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import com.fasterxml.jackson.databind.util.NameTransformer;
import java.io.IOException;
import java.util.UUID;
public class JsonPathApp {
public static void main(String[] args) throws Exception {
SimpleModule wrappersModule = new SimpleModule("requestWrapper");
wrappersModule.setSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (Request.class.isAssignableFrom(beanDesc.getBeanClass())) {
return new RequestWrapperJsonSerializer(serializer);
}
return serializer;
}
});
ObjectMapper mapper = JsonMapper.builder()
.enable(SerializationFeature.INDENT_OUTPUT)
.addModule(wrappersModule)
.build();
System.out.println(mapper.writeValueAsString(new Request1(1, "POST")));
System.out.println(mapper.writeValueAsString(new Request2(2, UUID.randomUUID())));
}
}
class RequestWrapperJsonSerializer extends JsonSerializer<Request> {
private final JsonSerializer baseSerializer;
public RequestWrapperJsonSerializer(JsonSerializer baseSerializer) {
this.baseSerializer = baseSerializer;
}
#Override
public void serialize(Request value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeFieldName(value.getClass().getSimpleName() + "Wrapper");
gen.writeStartObject();
baseSerializer.unwrappingSerializer(NameTransformer.NOP).serialize(value, gen, serializers);
gen.writeEndObject();
gen.writeEndObject();
}
}
abstract class Request {
private int id;
//constructor, getters, setters, toString
}
class Request1 extends Request {
private String body;
//constructor, getters, setters, toString
}
class Request2 extends Request {
private UUID uuid;
//constructor, getters, setters, toString
}
Above code prints:
{
"Request1Wrapper" : {
"id" : 1,
"body" : "POST"
}
}
{
"Request2Wrapper" : {
"id" : 2,
"uuid" : "dd4cccb5-1cf5-4dd4-8bc7-97cb101e5d7d"
}
}
Instead unwrappingSerializer method you can use serialize method and remove extra wrapping invocations.
Even though the solution I accepted is correct, I propose an other solution I got from a similar stackoverflow question, and which relies on a trick: make the class to custom-serialize a field of an other class (purely used for wrapping) and use #JsonSerialize on this field. The code is simpler, but you have to create and manipulate the wrapper class. See below:
public class RequestWrapper {
#JsonUnwrapped // Prevent the field from being wrap with its name: "request"
#JsonSerialize(using = RequestSerializer.class)
private final Request request;
private RequestWrapper(Request request) {
this.request= request;
}
public Request getRequest() {
return request;
}
public static class RequestSerializer extends JsonSerializer<Request> {
#Override
public boolean isUnwrappingSerializer() {
return true; // Indicates that we are creating an "unwrapping" serializer, because we added #JsonUnwrapped
}
#Override
public void serialize(Request request, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeObjectField(request.getClass().getSimpleName(), value);
}
}
}
Went down a path of creating an annotation that would dynamic determine whether a field should be serialized or not.
The annotation's implementation is as follows:
#Retention(RetentionPolicy.RUNTIME)
#JacksonAnnotationsInside
#JsonSerialize(using = HiddenFieldSerializer.class)
#Target(value = ElementType.FIELD)
public #interface Hidden {
}
Now the code for the Serializer:
public class HiddenFieldSerializer
extends StdSerializer<String>
implements ContextualSerializer {
public HiddenFieldSerializer() {
super(String.class);
}
#Override
public void serialize(String value,
JsonGenerator jgen,
SerializerProvider provider) {
try {
provider.defaultSerializeNull(jgen);
} catch (IOException e) {
}
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov,
BeanProperty property) {
return shouldHide() ?
new HiddenFieldSerializer() : new StringSerializer();
}
public boolean shouldHide() {
/* Simplifying this */
return Boolean.TRUE;
}
}
A little bit of code to show how it works:
public class Test {
static final ObjectMapper mapper = new ObjectMapper()
.setSerializationInclusion(Include.NON_NULL)
.setSerializationInclusion(Include.NON_EMPTY);
static class User {
#JsonProperty
String username;
#Hidden
#JsonProperty
String pin;
}
public static void main(String... args)
throws JsonProcessingException {
final POC.User u = new POC.User();
u.username = "harry_potter";
u.pin = "1298";
System.out.println(mapper.writeValueAsString(u));
}
}
And the output is as follows:
{"username":"harry_potter","pin":null}
How do I get the field pin to be removed from the serialization instead of it being null? Obviously setting the mapper's properties was of very little user in such a context. Any suggestions? Thoughts? Maybe the whole thing is a bad idea?
Ideally I should be able to see the following:
{"username":"harry_potter"}
It's not clear whether you want to ignore a given property statically or dynamically. Anyways, looks like you have over-engineered it.
First of all, I want to make sure that you came across #JsonIgnore before. If it doesn't suit your needs, you could define your custom ignore annotation as following:
#Retention(RetentionPolicy.RUNTIME)
public #interface Hidden {
}
Then pick the approach that best suit your needs:
Approach #1
Extend JacksonAnnotationIntrospector and override the method that checks for the ignore marker:
public class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
return super.hasIgnoreMarker(m) || m.hasAnnotation(Hidden.class);
}
}
Configure ObjectMapper to use your annotation introspector:
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new CustomAnnotationIntrospector());
The annotation introspection occurs only once per class so you can not dynamically change the criteria you use (if any). A similar example can be seen in this answer.
Approach #2
Extend BeanSerializerModifier to modify the properties that will be serialized:
public class CustomBeanSerializerModifier extends BeanSerializerModifier {
#Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
return beanProperties.stream()
.filter(property -> property.getAnnotation(Hidden.class) == null)
.collect(Collectors.toList());
}
}
Then add it to a Module and register it to your ObjectMapper:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new SimpleModule() {
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(new CustomBeanSerializerModifier());
}
});
This approach allows you to ignore properties dynamically.
I'm currently migrating some code from Jackson 1.x to Jackson 2.5 json mapper and came a long a problem that wasn't there in 1.x.
This is the setup (see code below):
interface IPet
class Dog implements IPet
IPet is annotated with #JsonTypeInfo and #JsonSubTypes
class Human has a property of type IPet that is annotated with #JsonSerialize(using=CustomPetSerializer.class)
The problem:
If I serialize an instance of Dog it works as expected (also the type info is added to the json string by Jackson).
However when I serialize an instance of the Human class an exception is thrown saying:
com.fasterxml.jackson.databind.JsonMappingException: Type id handling
not implemented for type com.pet.Dog (through reference chain:
com.Human["pet"])
The serialize(...) method of the CustomPetSerializer class is not invoked (tested using a breakpoint).
The code:
IPet implementation:
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY, property="type")
#JsonSubTypes({
#JsonSubTypes.Type(value=Dog.class, name="dog")
//,#JsonSubTypes.Type(value=Cat.class, name="cat")
//more subtypes here...
})
public interface IPet
{
public Long getId();
public String getPetMakes();
}
Dog implementation:
public class Dog implements IPet
{
#Override
public String getPetMakes()
{
return "Wuff!";
}
#Override
public Long getId()
{
return 777L;
}
}
Human who owns a dog:
public class Human
{
private IPet pet = new Dog();
#JsonSerialize(using=CustomPetSerializer.class)
public IPet getPet()
{
return pet;
}
}
CustomPetSerializer implementation:
public class CustomPetSerializer extends JsonSerializer<IPet>
{
#Override
public void serialize(IPet value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException
{
if(value != null && value.getId() != null)
{
Map<String,Object> style = new HashMap<String,Object>();
style.put("age", "7");
gen.writeObject(style);
}
}
}
JUnit test method:
#Test
public void testPet() throws JsonProcessingException
{
ObjectMapper mapper = new ObjectMapper();
Human human = new Human();
//works as expcected
String json = mapper.writeValueAsString(human.getPet());
Assert.assertNotNull(json);
Assert.assertTrue(json.equals("{\"type\":\"dog\",\"id\":777,\"petMakes\":\"Wuff!\"}"));
//throws exception: Type id handling not implemented for type com.pet.Dog (through reference chain: com.Human["pet"])
json = mapper.writeValueAsString(human); //exception is thrown here
Assert.assertNotNull(json);
Assert.assertTrue(json.contains("\"age\":\"7\""));
}
You'll need to additionally override serializeWithType within you CustomPetSerializer because IPet is polymorphic. That's also the reason why serialize is not called. Check this related SO question that explains in detail when serializeWithType is called. For instance, your serializeWithType implementation might look something like this:
#Override
public void serializeWithType(IPet value, JsonGenerator gen,
SerializerProvider provider, TypeSerializer typeSer)
throws IOException, JsonProcessingException {
typeSer.writeTypePrefixForObject(value, gen);
serialize(value, gen, provider); // call your customized serialize method
typeSer.writeTypeSuffixForObject(value, gen);
}
which will print {"pet":{"type":"dog":{"age":"7"}}} for your Human instance.
Since Jackson 2.9 writeTypePrefixForObject() and writeTypeSuffixForObject() have been deprecated (I'm unclear why). It seems under the new approach it would now be:
#Override
public void serializeWithType(IPet value, JsonGenerator gen,
SerializerProvider provider, TypeSerializer typeSer)
throws IOException, JsonProcessingException {
WritableTypeId typeId = typeSer.typeId(value, START_OBJECT);
typeSer.writeTypePrefix(gen, typeId);
serialize(value, gen, provider); // call your customized serialize method
typeSer.writeTypeSuffix(gen, typeId);
}
So an extra line now, so not sure why it's a step forward, perhaps it's more efficient reusing the typeId object.
Source: Jackson's ObjectNode class currently in master. Not the best source but couldn't see any upgrade docs explaining what to do.