Configure Objectmapper used by Java AWS Lambda - java

I am trying to develop an AWS Lambda function that is triggered by events from SQS.
I am using the spring-cloud-function-adapter-aws (version 1.0.0.RELEASE) and in specifically a SpringBootRequestHandler.
However, the ObjectMapper that is being used is case-sensitive and therefore failing to successful convert the Json coming from SQS.
SQS publishes the following Json and it is the Records field in particular that I'm having the problem with.
{
"Records": [
{
"body": "Hello from SQS!",
"receiptHandle": "MessageReceiptHandle",
"md5OfBody": "7b270e59b47ff90a553787216d55d91d",
"eventSourceARN": "arn:aws:sqs:eu-west-1:123456789012:MyQueue",
"eventSource": "aws:sqs",
"awsRegion": "eu-west-1",
"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
"attributes": {
"ApproximateFirstReceiveTimestamp": "1523232000001",
"SenderId": "123456789012",
"ApproximateReceiveCount": "1",
"SentTimestamp": "1523232000000"
},
"messageAttributes": {}
}
]
}
I have tried the suggestions in this question, but to no avail. Configuring ObjectMapper in Spring
In my POJO, I've also added the below annotation but it isn't working either whilst it would outside of Lambda.
#JsonProperty("Records")
private List<SqsRecord> Records;
Any help would be much appreciated.
My Lambda handler is defined as:
public class SqsEventHandler extends SpringBootRequestHandler<SqsEvent, String> {}
The POJO defined as:
public class SqsEvent {
#JsonProperty("Records")
private List<SqsRecord> records;
#Data
public class SqsRecord {
private String body;
private String receiptHandle;
private String md5OfBody;
private String eventSourceARN;
private String eventSource;
private String awsRegion;
private String messageId;
}
}
I expect the Json from the sample message to be able to be read in by the ObjectMapper, but the field "records" is null.

I got this issue solved in a more simple manner.
Referencing https://docs.aws.amazon.com/lambda/latest/dg/java-handler-io-type-stream.html and in specific
if Lambda's serialization approach does not meet your needs, you can use the byte stream implementation
I am now using the SpringBootStreamHandler directly and I have created an ObjectMapper instance with my required configuration options in my Spring Configuration class as:
#Bean
public ObjectMapper objectMapper() {
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return mapper;
}

we've got this problem with a lot of AWS services.
You must define a new mapper like this :
SQSMixin :
private static interface SQSEventMixin {
public static final String ATTRIBUTES = "attributes";
public static final String AWS_REGION = "awsRegion";
public static final String BODY = "body";
public static final String EVENT_SOURCE = "eventSource";
public static final String EVENT_SOURCE_ARN = "eventSourceARN";
public static final String MD5_OF_BOBY = "md5OfBody";
public static final String MD5_OF_MESSAGE_ATTRIBUTES = "md5OfMessageAttributes";
public static final String MESSAGE_ID = "messageId";
public static final String RECEIPT_HANDLE = "receiptHandle";
#JsonProperty(value = "Records")
public List<?> getRecords();
static interface MessageMixin {
#JsonProperty(ATTRIBUTES)
public String getAttributes();
#JsonProperty(ATTRIBUTES)
public void setAttributes(String attributes);
#JsonProperty(AWS_REGION)
public String getAwsRegion();
#JsonProperty(AWS_REGION)
public void setAwsRegion(String awsRegion);
#JsonProperty(BODY)
public Object getBody();
#JsonProperty(BODY)
public void setBody(Object body);
#JsonProperty(EVENT_SOURCE)
public String getEventSource();
#JsonProperty(EVENT_SOURCE)
public void setEventSource(String eventSource);
#JsonProperty(EVENT_SOURCE_ARN)
public String getEventSourceArn();
#JsonProperty(EVENT_SOURCE_ARN)
public void setEventSourceArn(String eventSourceArn);
#JsonProperty(MD5_OF_BOBY)
public String getMd5OfBody();
#JsonProperty(MD5_OF_BOBY)
public void setMd5OfBody(String md5OfBody);
#JsonProperty(MD5_OF_MESSAGE_ATTRIBUTES)
public String getMd5OfMessageAttributes();
#JsonProperty(MD5_OF_MESSAGE_ATTRIBUTES)
public void setMd5OfMessageAttributes(String md5OfMessageAttributes);
#JsonProperty(MESSAGE_ID)
public String getMessageId();
#JsonProperty(MESSAGE_ID)
public void setMessageId(String messageId);
#JsonProperty(RECEIPT_HANDLE)
public String getReceiptHandle();
#JsonProperty(RECEIPT_HANDLE)
public void setReceiptHandle(String receiptHandle);
}
}
A Strategy for record :
private static class UpperCaseRecordsPropertyNamingStrategy extends PropertyNamingStrategy.PropertyNamingStrategyBase {
private static final long serialVersionUID = 1L;
#Override
public String translate(String propertyName) {
if (propertyName.equals("records")) {
return "Records";
}
return propertyName;
}
}
Formatter for Date :
private static final DateTimeFormatter dateTimeFormatter = ISODateTimeFormat.dateTime()
.withZone(new FixedDateTimeZone("GMT", "GMT", 0, 0));
private static class DateTimeMapperModule extends SimpleModule {
private static final long serialVersionUID = 1L;
public DateTimeMapperModule() {
super("DateTimeMapperModule");
super.addSerializer(DateTime.class, new DateTimeSerializer());
super.addDeserializer(DateTime.class, new DateTimeDeserializer());
}
}
private static class DateTimeSerializer extends JsonSerializer<DateTime> {
#Override
public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(dateTimeFormatter.print(value));
}
}
private static class DateTimeDeserializer extends JsonDeserializer<DateTime> {
#Override
public DateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {
return dateTimeFormatter.parseDateTime(parser.getText());
}
}
And declare your mapper :
ObjectMapper mapper = new ObjectMapper();
mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
mapper.setPropertyNamingStrategy(new UpperCaseRecordsPropertyNamingStrategy());
mapper.registerModule(new DateTimeMapperModule());
mapper.addMixIn(SQSMessage.class, SQSEventMixin.MessageMixin.class);
SQSEvent request = mapper.convertValue(inputObject, SQSEvent.class);

There is already an official library that is supporting this: https://aws.amazon.com/blogs/opensource/testing-aws-lambda-functions-written-in-java/
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-tests</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>
Also have surefire in your plugins:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
Example:
SQSEvent input = EventLoader.loadEvent("/sqsEvent.json", SQSEvent.class);

Related

Java convert object into abstract object

I am using SpringBoot with Java 1.8.
I have two objects that I would like to deep copy one to the other.
Basic structure
QuoteRequestDTO -> TravelRequirementDTO -> ItineraryDTO -> ServiceDTO
and
QuoteRequest -> TravelRequirement -> Itinerary -> Service
note: the Entity objects come from an external library I cannot change. I can change the DTO objects.
For example, I want to copy a DTO to an Entity.
DTO
public class QuoteRequestDTO {
protected TravelRequirementDTO travel;
...
and
public class TravelRequirementDTO {
protected ItineraryDTO required;
...
and
public class ItineraryDTO extends PayableDTO {
protected List<ServiceDTO> service;
...
and
public class ServiceDTO extends PayableDTO {
...
Entity
public class QuoteRequest {
protected TravelRequirement travel;
...
and
public class TravelRequirement implements Serializable {
private static final long serialVersionUID = 1L;
protected Itinerary required;
...
and
public class Itinerary extends Payable implements Serializable {
private static final long serialVersionUID = 1L;
protected List<Service> service;
...
and
public abstract class Service extends Payable implements Serializable {
private static final long serialVersionUID = 1L;
...
Copy Utility
I have tried the following:
import com.fasterxml.jackson.databind.ObjectMapper;
public class CopyUtils {
public static <T>T deepCopy(Object sourceObject, T targetObject) {
ObjectMapper mapper = getJacksonObjectMapper();
T targetBean = (T) mapper.convertValue(sourceObject, targetObject.getClass());
return targetBean;
}
private static ObjectMapper getJacksonObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
return objectMapper;
}
}
Usage:
public void getQuote(QuoteRequestDTO quoteRequestDTO) {
QuoteRequest quoteRequest = new QuoteRequest();
quoteRequest = CopyUtils.deepCopy(quoteRequestDTO, quoteRequest);
Error
It gets the following error:
Cannot construct instance of com.mycompany.transit._2008a.Service
(no Creators, like default constructor, exist): abstract types either
need to be mapped to concrete types, have custom deserializer, or
contain additional type information at [Source: UNKNOWN; byte offset:
#UNKNOWN] (through reference chain: com.mycompany.transit._2008a.availability.QuoteRequest["travel"]->com.mycompany.transit._2008a.TravelRequirement["required"]->com.mycompany.transit._2008a.Itinerary["service"]->java.util.ArrayList[0])
When I change the ServiceDTO to be an abstract class:
public abstract class ServiceDTO extends PayableDTO implements Serializable {
private static final long serialVersionUID = 1L;
It get the following error:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of com.mycompany.restosgi.dto.transit.ServiceDTO
(no Creators, like default constructor, exist): abstract types either
need to be mapped to concrete types, have custom deserializer, or
contain additional type information at [Source:
(org.springframework.util.StreamUtils$NonClosingInputStream); line:
13, column: 13] (through reference chain:
com.mycompany.restosgi.dto.transit.availability.QuoteRequestDTO["travel"]->com.mycompany.restosgi.dto.transit.TravelRequirementDTO["required"]->com.mycompany.restosgi.dto.transit.ItineraryDTO["service"]->java.util.ArrayList[0])
Question
Is there a way I can write a generic utility method to deep copy objects to another object that has abstract objects?
Possible solution
Is there a way to add a converter that creates the relevant concrete class (implementation)?
The Service needs to be an implementation, for example a TransitService.
e.g.
public class TransitService extends Service implements Serializable {
private static final long serialVersionUID = 1L;
Possible Solution
As per the advise from Delta George below, I am trying the following:
public class ItineraryDTO extends PayableDTO {
protected List<ServiceDTO> service;
#JsonAnySetter
public void serService(String key, ArrayNode array) {
service = new ArrayList<>();
array.forEach(json -> service.add(toService(json)));
}
private ServiceDTO toService(JsonNode json) {
if (json.has("some unique property of flight")) {
return new ObjectMapper().convertValue(json, FlightDTO.class);
} else if (json.has("some unique property of transit")) {
return new ObjectMapper().convertValue(json, TransitServiceDTO.class);
} else return null;
}
...
However, I cannot get it to invoke the serService method. I think I need to add some Spring config to do so?
Also, if I make the ServiceDTO abstract, it get the following error.
public abstract class ServiceDTO extends PayableDTO {
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of com.clubtravel.restosgi.dto.transit.ServiceDTO
(no Creators, like default constructor, exist): abstract types either
need to be mapped to concrete types, have custom deserializer, or
contain additional type information at [Source:
(org.springframework.util.StreamUtils$NonClosingInputStream); line:
13, column: 13] (through reference chain:
com.clubtravel.restosgi.dto.transit.availability.QuoteRequestDTO["travel"]->com.clubtravel.restosgi.dto.transit.TravelRequirementDTO["required"]->com.clubtravel.restosgi.dto.transit.ItineraryDTO["service"]->java.util.ArrayList[0])
at
com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
~[jackson-databind-2.13.2.1.jar:2.13.2.1]
A concept implementation using Jackson's #JsonAnySetter to intercept incoming objects as an array of JSON trees and convert each to a concrete POJO based on the structure of each object:
#Data
#NoArgsConstructor
static abstract class Person {
}
#Data
#NoArgsConstructor
static class PersonA extends Person {
String a;
}
#Data
#NoArgsConstructor
static class PersonB extends Person {
String b;
}
#NoArgsConstructor
#ToString
static class People {
List<Person> team;
#JsonAnySetter
public void setTeam(String key, ArrayNode array) {
team = new ArrayList<>();
array.forEach(json -> team.add(toPerson(json)));
}
private Person toPerson(JsonNode json) {
if (json.has("a")) {
return new ObjectMapper().convertValue(json, PersonA.class);
} else if (json.has("b")) {
return new ObjectMapper().convertValue(json, PersonB.class);
} else return null;
}
}
public static void main(String[] args) throws JsonProcessingException {
String json = "{\"team\": [{\"a\": 123}, {\"b\": 45}]}";
People people = new ObjectMapper().readValue(json, People.class);
System.out.println(people);
// Prints: People(team=[PersonA(a=123), PersonB(b=45)])
}
// back to the OP's data model
public static class QuoteRequest {
protected TravelRequirement travel;
public TravelRequirement getTravel() {
return travel;
}
public void setTravel(TravelRequirement travel) {
this.travel = travel;
}
}
public static class TravelRequirement implements Serializable {
private static final long serialVersionUID = 1L;
protected Itinerary required;
public Itinerary getRequired() {
return required;
}
public void setRequired(Itinerary required) {
this.required = required;
}
}
public static class Itinerary extends Payable implements Serializable {
private static final long serialVersionUID = 1L;
protected List<Service> service;
public void service(List<Service> service) {
this.service = service;
}
#JsonAnyGetter
public Map<String, Object> getService() {
return Map.of("service", service);
}
#JsonAnySetter
public void setService(String key, ArrayNode array) {
service = new ArrayList<>();
array.forEach(json -> service.add(toService(json)));
}
private Service toService(JsonNode json) {
return getJacksonObjectMapper().convertValue(json, TransitService.class);
}
}
public static abstract class Service extends Payable implements Serializable {
private static final long serialVersionUID = 1L;
}
public static class TransitService extends Service implements Serializable {
private static final long serialVersionUID = 1L;
}
public static class Payable implements Serializable {
private static final long serialVersionUID = 1L;
}
private static ObjectMapper getJacksonObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
return objectMapper;
}
public static void main(String[] args) throws JsonProcessingException {
QuoteRequest qr = new QuoteRequest();
TravelRequirement tr = new TravelRequirement();
Itinerary i = new Itinerary();
i.service(List.of(new TransitService()));
tr.setRequired(i);
qr.setTravel(tr);
ObjectMapper mapper = getJacksonObjectMapper();
QuoteRequest qr2 = mapper.convertValue(qr, QuoteRequest.class);
System.out.println(qr2);
}

How to access the gson SerializedName annotaion value from properties file

I am trying to de-serialize my json string to my custom class ,I want to make the
#SerializedName annotation value to accessed as configurable parameter which should read values from application properties file .I have provided the below code snippet ,But it is accepting a constant string, is there any alternative way to make these parameter's as configurable
{
FName: "Sample",
LName: "LName"}
class Test{
#SerializedName(value=${"${name}"})
private string name;
#SerializedName(value=${"${data}"})
private string data;
}
application.properties file
name=FName
data=LName
Gson is not aware of any other library unless you tell it to do so (why pay for what you might never use otherwise?). You can extend Gson on top of what Gson provides using a custom implementation of the FieldNamingStrategy interface:
public final class ValueFieldNamingStrategy
implements FieldNamingStrategy {
private final Environment environment;
private ValueFieldNamingStrategy(final Environment environment) {
this.environment = environment;
}
public static FieldNamingStrategy of(final Environment environment) {
return new ValueFieldNamingStrategy(environment);
}
#Override
public String translateName(final Field f) {
#Nullable
final Value valueAnnotation = f.getAnnotation(Value.class);
if ( valueAnnotation == null ) {
return f.getName();
}
return environment.resolvePlaceholders(valueAnnotation.value());
}
}
public final class ValueFieldNamingStrategyTest {
#org.junit.jupiter.api.Test
public void test()
throws IOException {
final ConfigurableEnvironment configurableEnvironment = new StandardEnvironment();
final MutablePropertySources propertySources = configurableEnvironment.getPropertySources();
final PropertySource<?> propertySource = new MapPropertySource("test", ImmutableMap.of(
"name", "FName",
"data", "LName"
));
propertySources.addLast(propertySource);
final FieldNamingStrategy unit = ValueFieldNamingStrategy.of(configurableEnvironment);
final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
.disableInnerClassSerialization()
.setFieldNamingStrategy(unit)
.create();
try ( final JsonReader jsonReader = /*...open JSON reader here...*/ ) {
final Test test = gson.fromJson(jsonReader, Test.class);
Assertions.assertEquals("FName", test.name);
Assertions.assertEquals("LName", test.data);
}
}
private static final class Test {
#Value("${name}")
private String name;
#Value("${data}")
private String data;
}
}

Jackson -- deserialize JavaFX StringProperty type field with custom deserializer

Let's say my POJO is limited to the following:
public class TestPojo {
StringProperty myField;
public TestPojo(){}
public TestPojo(String myField) {
this.myField = new SimpleStringProperty(myField);
}
public String getMyField() {
return this.myField.get();
}
public StringProperty myFieldProperty() {
return this.myField;
}
public void setMyField(String myField) {
this.myField.set(myField);
}
}
I want to deserialize with Jackson. Jackson doesn't seem to like StringProperty, because as a normal String it works as expected. So I write a custom deserializer...
public class StringPropertyDeserializer extends JsonDeserializer<StringProperty> {
public StringPropertyDeserializer() {}
#Override
public StringProperty deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
String value = jsonParser.readValueAs(String.class);
return new SimpleStringProperty(value);
}
}
And I add this annotation to my StringProperty field...
#JsonDeserialize(using = StringPropertyDeserializer.class)
I get the following error:
com.fasterxml.jackson.databind.JsonMappingException: Class com.test.example.TestPojo$StringPropertyDeserializer has no default (no arg) constructor
I added a default constructor into the deserializer (as you can see above) and I get the same error regardless of it being there. Any solutions? This seems like it should be simple.
Edit here is the actual code where I'm making the call if that's helpful...
#GET("/api/test")
Call<List<TestPojo>> testPojoCall();
And it's configured in Gradle like so:
compile group: 'com.squareup.retrofit2', name: 'converter-jackson', version: '2.1.0'
That is everything related to this code.
Your POJO implementation isn't correct: the no-arg constructor doesn't initialize the StringProperty. Consequently, if you use the no-arg constructor, then call setMyField(...) you'd get a null pointer exception. I'm guessing that trying to work around that was what led you to try creating the custom deserializer.
I'm not entirely sure why the custom deserializer is giving the error it's giving (maybe it doesn't like the fact that it's an inner class: that's just a guess).
The following works fine for me without any custom deserialization:
TestPojo.java:
package jxtest;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class TestPojo {
private final StringProperty myField1 = new SimpleStringProperty();
private final StringProperty myField2 = new SimpleStringProperty();
private int myField3 ;
public final StringProperty myField1Property() {
return this.myField1;
}
public final String getMyField1() {
return this.myField1Property().get();
}
public final void setMyField1(final String myField1) {
this.myField1Property().set(myField1);
}
public final StringProperty myField2Property() {
return this.myField2;
}
public final String getMyField2() {
return this.myField2Property().get();
}
public final void setMyField2(final String myField2) {
this.myField2Property().set(myField2);
}
public int getMyField3() {
return myField3;
}
public void setMyField3(int myField3) {
this.myField3 = myField3;
}
}
Test.java:
package jxtest;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Test {
public static void main(String[] args) throws IOException {
TestPojo item = new TestPojo();
item.setMyField1("Test1");
item.setMyField2("Test2");
item.setMyField3(42);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(item);
System.out.println(json);
TestPojo readBackIn = mapper.readValue(json, TestPojo.class);
System.out.println(readBackIn.getMyField1());
System.out.println(readBackIn.getMyField2());
System.out.println(readBackIn.getMyField3());
}
}
Output:
{"myField1":"Test1","myField2":"Test2","myField3":42}
Test1
Test2
42
Using jackson-databind 2.8.7.

jackson: ignore getter, but not with #JsonView

I'm looking for possibility to serialize transient information only in some cases:
#JsonInclude(Include.NON_NULL)
#Entity
public class User {
public static interface AdminView {}
... id, email and others ...
#Transient
private transient Details details;
#JsonIgnore // Goal: ignore all the time, except next line
#JsonView(AdminView.class) // Goal: don't ignore in AdminView
public Details getDetails() {
if (details == null) {
details = ... compute Details ...
}
return details;
}
}
public class UserDetailsAction {
private static final ObjectWriter writer = new ObjectMapper();
private static final ObjectWriter writerAdmin = writer
.writerWithView(User.AdminView.class);
public String getUserAsJson(User user) {
return writer.writeValueAsString(user);
}
public String getUserAsJsonForAdmin(User user) {
return writerAdmin.writeValueAsString(user);
}
}
If I call getUserAsJson I expected to see id, email and other fields, but not details. This works fine. But I see same for getUserAsJsonForAdmin, also without detail. If I remove #JsonIgnore annotation - I do see details in both calls.
What do I wrong and is there good way to go? Thanks!
You may find the use of the dynamic Jackson filtering slightly more elegant for your use case. Here is an example of the filtering of POJO fields based on a custom annotation sharing one object mapper instance:
public class JacksonFilter {
static private boolean shouldIncludeAllFields;
#Retention(RetentionPolicy.RUNTIME)
public static #interface Admin {}
#JsonFilter("admin-filter")
public static class User {
public final String email;
#Admin
public final String details;
public User(String email, String details) {
this.email = email;
this.details = details;
}
}
public static class AdminPropertyFilter extends SimpleBeanPropertyFilter {
#Override
protected boolean include(BeanPropertyWriter writer) {
// deprecated since 2.3
return true;
}
#Override
protected boolean include(PropertyWriter writer) {
if (writer instanceof BeanPropertyWriter) {
return shouldIncludeAllFields || ((BeanPropertyWriter) writer).getAnnotation(Admin.class) == null;
}
return true;
}
}
public static void main(String[] args) throws JsonProcessingException {
User user = new User("email", "secret");
ObjectMapper mapper = new ObjectMapper();
mapper.setFilters(new SimpleFilterProvider().addFilter("admin-filter", new AdminPropertyFilter()));
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user));
shouldIncludeAllFields = true;
System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user));
}
}
Output:
{
"email" : "email"
}
{
"email" : "email",
"details" : "secret"
}
It's look like jackson have horrible concept on very cool feature like #JsonView. The only way I discover to solve my problem is:
#JsonInclude(Include.NON_NULL)
#Entity
public class User {
public static interface BasicView {}
public static interface AdminView {}
... id and others ...
#JsonView({BasicView.class, AdminView.class}) // And this for EVERY field
#Column
private String email;
#Transient
private transient Details details;
#JsonView(AdminView.class)
public Details getDetails() {
if (details == null) {
details = ... compute Details ...
}
return details;
}
}
public class UserDetailsAction {
private static final ObjectWriter writer = new ObjectMapper()
.disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.writerWithView(User.BasicView.class);
private static final ObjectWriter writerAdmin = new ObjectMapper()
.disable(MapperFeature.DEFAULT_VIEW_INCLUSION)
.writerWithView(User.AdminView.class);
public String getUserAsJson(User user) {
return writer.writeValueAsString(user);
}
public String getUserAsJsonForAdmin(User user) {
return writerAdmin.writeValueAsString(user);
}
}
Maybe it's help some one. But I hope to find better solution and because doesn't accept my own answer.
EDIT: because interface can extends (multiple) interfaces, I can use:
public static interface AdminView extends BasicView {}
and just
#JsonView(BasicView.class)
instead of
#JsonView({BasicView.class, AdminView.class})

Jackson dynamic property names

I would like serialize an object such that one of the fields will be named differently based on the type of the field. For example:
public class Response {
private Status status;
private String error;
private Object data;
[ getters, setters ]
}
Here, I would like the field data to be serialized to something like data.getClass.getName() instead of always having a field called data which contains a different type depending on the situation.
How might I achieve such a trick using Jackson?
I had a simpler solution using #JsonAnyGetter annotation, and it worked like a charm.
import java.util.Collections;
import java.util.Map;
public class Response {
private Status status;
private String error;
#JsonIgnore
private Object data;
[getters, setters]
#JsonAnyGetter
public Map<String, Object> any() {
//add the custom name here
//use full HashMap if you need more than one property
return Collections.singletonMap(data.getClass().getName(), data);
}
}
No wrapper needed, no custom serializer needed.
Using a custom JsonSerializer.
public class Response {
private String status;
private String error;
#JsonProperty("p")
#JsonSerialize(using = CustomSerializer.class)
private Object data;
// ...
}
public class CustomSerializer extends JsonSerializer<Object> {
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeObjectField(value.getClass().getName(), value);
jgen.writeEndObject();
}
}
And then, suppose you want to serialize the following two objects:
public static void main(String... args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
Response r1 = new Response("Error", "Some error", 20);
System.out.println(mapper.writeValueAsString(r1));
Response r2 = new Response("Error", "Some error", "some string");
System.out.println(mapper.writeValueAsString(r2));
}
The first one will print:
{"status":"Error","error":"Some error","p":{"java.lang.Integer":20}}
And the second one:
{"status":"Error","error":"Some error","p":{"java.lang.String":"some string"}}
I have used the name p for the wrapper object since it will merely serve as a placeholder. If you want to remove it, you'd have to write a custom serializer for the entire class, i.e., a JsonSerializer<Response>.
my own solution.
#Data
#EqualsAndHashCode
#ToString
#JsonSerialize(using = ElementsListBean.CustomSerializer.class)
public class ElementsListBean<T> {
public ElementsListBean()
{
}
public ElementsListBean(final String fieldName, final List<T> elements)
{
this.fieldName = fieldName;
this.elements = elements;
}
private String fieldName;
private List<T> elements;
public int length()
{
return (this.elements != null) ? this.elements.size() : 0;
}
private static class CustomSerializer extends JsonSerializer<Object> {
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonProcessingException
{
if (value instanceof ElementsListBean) {
final ElementsListBean<?> o = (ElementsListBean<?>) value;
jgen.writeStartObject();
jgen.writeArrayFieldStart(o.getFieldName());
for (Object e : o.getElements()) {
jgen.writeObject(e);
}
jgen.writeEndArray();
jgen.writeNumberField("length", o.length());
jgen.writeEndObject();
}
}
}
}
You can use the annotation JsonTypeInfo, which tell Jackson exactly that and you don't need to write a custom serializer. There's various way to include this information, but for your specific question you'd use As.WRAPPER_OBJECT and Id.CLASS. For example:
public static class Response {
private Status status;
private String error;
#JsonTypeInfo(include = As.WRAPPER_OBJECT, use = Id.CLASS)
private Object data;
}
This, however, will not work on primitive type, such as a String or Integer. You don't need that information for primitives anyways, since they are natively represented in JSON and Jackson knows how to handle them. The added bonus with using the annotation is that you get deserialization for free, if you ever need it. Here's an example:
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
Response r1 = new Response("Status", "An error", "some data");
Response r2 = new Response("Status", "An error", 10);
Response r3 = new Response("Status", "An error", new MyClass("data"));
System.out.println(mapper.writeValueAsString(r1));
System.out.println(mapper.writeValueAsString(r2));
System.out.println(mapper.writeValueAsString(r3));
}
#JsonAutoDetect(fieldVisibility=Visibility.ANY)
public static class MyClass{
private String data;
public MyClass(String data) {
this.data = data;
}
}
and the result:
{"status":"Status","error":"An error","data":"some data"}
{"status":"Status","error":"An error","data":10}
{"status":"Status","error":"An error","data":{"some.package.MyClass":{"data":"data"}}}
Based on #tlogbon response,
Here is my solution to wrap a List of Items with a specific/dynamic filed name
public class ListResource<T> {
#JsonIgnore
private List<T> items;
#JsonIgnore
private String fieldName;
public ListResource(String fieldName, List<T> items) {
this.items = items;
this.fieldName = fieldName;
}
#JsonAnyGetter
public Map<String, List<T>> getMap() {
return Collections.singletonMap(fieldName, items);
}

Categories

Resources