I'm using Spring cloud + Swagger to build a SaaS system, which needs Stripe SDK to take payment.
The problem is that Stripe's SDK uses Gson annotations to shape the JSON serialized models. For example:
package com.stripe.model;
public class Dispute extends ApiResource implements MetadataStore<Dispute>, BalanceTransactionSource {
#SerializedName("balance_transactions")
List<BalanceTransaction> balanceTransactions;
}
I declare a REST api to return a Dispute:
class StripeController {
#GetMapping("/disputes")
Dispute getDispute(#RequestParam String id) {
Dispute dispute = stripeService.getDispute(id);
}
}
I hope this REST api prints {"balance_transactions":"..."} rather than {"balanceTransactions":"..."}.
At first, I write a StripeObjectSerializer to let Jackson serialize it as expected in response:
public class StripeObjectSerializer extends JsonSerializer<StripeObject> {
#Override
public void serialize(StripeObject stripeObject, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeRawValue(stripeObject.toJson());
}
}
It basically works!
However, Swagger cannot recognize StripeObjectSerializer, it creates models like {"balanceTransactions":"..."} with so many extra useless properties, such as lastResponse.
What should I do?
Related
#JsonSerialize(using = TestDefSerializer.class)
public class TestDef{
private List<TestStep> steps = new LinkedList<>();
private String name;
} //Getter and Setters are defined
I can't seem to figure out a way after this
public class TestDefSerializer extends StdSerializer<TestDef> {
public TestDefSerializer(Class<TestDef> t) {
super(t);
}
public TestDefSerializer(){
this(TestDef.class);
}
#Override
public void serialize(TestDef testDefinition, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
// What should go here in order to serialize List<TestStep> ???
}
}
TestStep has a couple of fields namely responseDef (which has path , method) , requestDef (mathcing , status) etc. I wish to skip a couple of fields in responseDef and requestDef
There are chances you don't even need to implement a
TestDefSerializer at all , because Jackson is probably
smart enough to pick up enough information from the getters
of your TestDef class.
Just omit the line
#JsonSerialize(using = TestDefSerializer.class)
on your TestDef class and check if this will already
produce the JSON output you want.
But anyway, here is how to proceed if you want to
implement your own TestDefSerializer.
Look up the the API docu of JsonGenerator.
It describes all the write... methods available
for writing the JSON pieces.
For example, there is method writeStartArray() for writing a [,
and writeEndArray() for writing a ].
So in your TestDefSerializer class you may end up
with a serialize method looking like this:
#Override
public void serialize(TestDef testDefinition, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject(); // write {
jsonGenerator.writeFieldName("name"); // write "name":
jsonGenerator.writeString(testDefinition.getName());
jsonGenerator.writeFieldName("steps"); // write "steps":
jsonGenerator.writeStartArray(); // write [
for (TestStep testStep : testDefinition.getSteps()) {
jsonGenerator.writeObject(testStep); // this will invoke the serializer for TestStep
}
jsonGenerator.writeEndArray(); // write ]
jsonGenerator.writeEndObject(); // write }
}
Jackson docs say that a class that implements their JsonSerializable interface will be serialized by calling the Interface's serialize() method.
I tried this in a project that uses Jackson 2.8.4 under Jersey 2.25.
It continues to use Jackson's BeanSerializer to do default serialization based on public getters, instead of using the SerializableSerializer and the serialize() method.
Code looks like this fairly minimal example...
public class Minimal extends JsonSerializable.Base {
private String title;
private ZonedDateTime theTime;
public String getTitle() { return title; }
void setTitle(String title) { this.title = title; }
public ZonedDateTime getTheTime() { return theTime; }
void setTheTime(ZonedDateTime theTime) { this.theTime = theTime; }
#Override
public void serialize(JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeFieldName("title");
gen.writeString(this.title);
// Other serialization...
gen.writeEndObject();
}
#Override
public void serializeWithType(JsonGenerator gen, SerializerProvider serializers, TypeSerializer typeSer) throws IOException {
throw new UnsupportedOperationException("Not supported.");
}
}
I also tried other ideas to get it using the right serializer...maybe foolish ideas like...
public class Minimal implements JsonSerializable {
and
#JsonSerialize(using = SerializableSerializer.class)
public class Minimal extends JsonSerializable.Base {
and
#JsonSerialize(as = JsonSerializable.Base.class)
public class Minimal extends JsonSerializable.Base {
What am I missing to get this Jackson feature working?
I verified in Jackson's latest source code that the feature still exists. I suppose I can trace it through the 2.8.4 source in the debugger, but maybe someone knows the answer.
Apparently the answer to
"What am I missing?"
is Nothing.
After writing the question, I rebuilt everything again, restarted Tomcat, and redeployed and tested again and got my expected output.
So I will chalk this up to bad build, bad deploy, confused Classloader, something like that. I am leaving the question, since it provides an example that might help someone.
I have a Java web application which uses Jackson 2.x to deserialize JSON requests. I'm currently interfacing with an external application which is sending JSON data with improperly formatted dates in one property of one of the classes. This is causing exceptions with that client's calls (as well it should). However, for business reasons we need a temporary workaround to accept these requests.
Until that client fixes the data it sends to my application (which may be a while), I want to treat any bad dates in that property as null. I do not want to change the actual class itself, as it is a public API which exposed to other clients, and I don't want this temporary workaround included in that class.
Is there an easy way to configure Jackson to treat invalid dates as null? I'm seeing similar functionality in DeserializationFeature, but nothing specifically for this.
A solution that would require you overriding the setter of the original class in a subclass:
public class InvalidDateDeserializer extends JsonDeserializer<Date>
{
#Override
public Date deserialize(JsonParser jsonParser,
DeserializationContext deserializationcontext) throws IOException, JsonProcessingException {
SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy'T'HH:mm:ss");
String date = jsonParser.getText();
try {
return sdf.parse(date);
} catch (ParseException e) {
return null;
}
}
}
Afterwards, you would just need to annotate the overriden setter in your class with
#JsonDeserialize(using = InvalidDateDeserializer.class)
So the new DTO class would look like this:
public MyModel extends TheirModel {
#Override
#JsonDeserialize(using = InvalidDateDeserializer.class)
public void setProblematicDate() {
super.setProblematicDate();
}
}
I understand this is no magic flag solution, but it should work in your case without changes to the original DTO class.
I have a class Organization with few date fields as follows.
public class Organization {
private String _id;
private String name;
#JsonDeserialize(using=JsonDateDeserializer.class)
#JsonSerialize(using=JsonDateSerializer.class)
private Date createdTime;
// getters and setters
}
To handle the date in simple way on client side I convert date to long and send it to client using these JsonSerializer as follows
public class JsonDateSerializer extends JsonSerializer<Date>{
#Override
public void serialize(Date date, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException {
gen.writeNumber(date.getTime());
}
}
I have a endpoint which will execute get organization and insert the results in response map like this.
#ApiMethod(name="organization.get", path="organizations/{organizationId}", httpMethod=HttpMethod.GET)
public Map<String, Object> organizationDetails(#Named("organizationId") String organizationId){
Organization org = DB.getOrganization("123");
Map<String, Object> response = new HashMap<String, Object>();
response.put("status", HttpServletResponse.SC_OK);
response.put("success", true);
response.put("entity", org);
return response;
}
But the end resultant JSON I see on client side is
{
"status" : 200,
"entity" : [ {
"_id" : "966a03b3-8e46-41ee-b330-6533409b2b4a",
"name" : "POKURI",
"createdTime" : "2015-05-16T15:02:31.499+05:30"
} ],
"success" : true
}
Here the date coming in some format instead of long. If I convert the same using Jackson ObjectMapper without using cloud endpoints I am getting response in expected way. Why cloud endpoints not respecting Jackson annotations? Is there a way to configure that?
Note: Even I observed that long is coming as string on client side if you use cloud endpoints. I am using appengine SDK 1.9.19
You aren't supposed to use Jackson annotations with Endpoints (and if you do, you have to use the repackaged version, not your own). You can do what you want with #ApiResourceProperty or #ApiTransformer, depending on how you want to do it. See the annotation documentation here.
Regarding long, it is serialized as a string due to floating point imprecision--it's not possible to represent all values of a long accurately using a double, which is what JavaScript and JSON.parse will store a numeric in, so it is always transmitted as a string. Any of our client libraries for Endpoints automatically convert them to the correct data type.
Deserializing works fine if I just pass my custom object through
#POST
public Response saveCustomObject(CustomObject data)
{
// Prints correct value
System.out.println(data);
}
However, if it is a property on another object, it just gets the default value of my custom object
#POST
public Response saveCustomObjectWrapper(CustomObjectWrapper data)
{
// Prints incorrect value
System.out.println(data.getCustomObject());
}
My provider is registered and looks like this:
public CustomObject readFrom(Class<CustomObject> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, String> mm, InputStream in) throws IOException, WebApplicationException
{
try {
return new CustomObject(IOUtils.toString(in));
} catch (Exception ex) {
throw new ProcessingException("Error deserializing a CustomObject.", ex);
}
}
The problem is that the reader for all other objects doesn't do lookup/delegation while unmarshalling. What I mean by that, can be seen in this answer, where one reader looks up another reader based on the type. Assuming the format is JSON, whether you're using MOXy (the default with Glassfish) or Jackson, the result is the same. The reader is smart enough to handle the the JSON by itself, so doesn't need to lookup any other readers.
One solution would be to create another reader for the wrapper class, and do lookup/delegation, as seen in the link above. If you have a lot of these situations, you may can extend the default reader, and override its unmarshalling method, but I would completely advise against this, unless you really know what you're doing.
Another solution, depending on the serializer you're using, is to write JsonDeserializer (for Jackson) or XmlAdapter (for MOXy or Jackson). For Jackson an example would be something like (you can see a better example here)
public class CustomObjectDeserializer extends JsonDeserializer<CustomObject> {
#Override
public CustomObject deserialize(JsonParser jp, DeserializationContext dc)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
return new CustomObject("Hello World");
}
}
#JsonDeserialize(using = CustomObjectDeserializer.class)
public class CustomObject {
public String message;
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public CustomObject(String message) { this.message = message; }
public CustomObject(){}
}
In which case, there is no need for a custom reader at all. This will handle CustomObjects and objects that have CustomObject as a member. One problem with this is I'm not sure how or if you can get the InputStream. You just need to use the Jackson APIs to parse the JSON.
If you want to use Jackson instead of the default MOXy for glassfish, you can just add the Jackson dependency
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.13</version>
</dependency>
Then register the JacksonFeature, or simply disable MOXy, as mentioned here. If you want to continue using MOXy, I don't know if there is such thing as a class level adapter, so you will still need the reader as well as create a XmlAdapter for class members. It's a bit of a hassle, but that's why I recommend Jackson, for many other reasons, besides this particular use case. You can see an example of an adapter here
Now a lot of this answer is based on the assumption you are using JSON format, as you haven't specified the media type you are using. If it some other format, then I think maybe your only solution is to create another customer reader for the wrapper.