I have a value object with an Optional<Integer> field. I want it to look like a string when it's serialized, but the Optional part is getting in the way.
I have the following class:
public class Person {
private com.google.common.base.Optional<Integer> age;
public com.google.common.base.Optional<Integer> getAge() {
return age;
}
public void setAge(Integer age) {
this.age = com.google.common.base.Optional.fromNullable(age);
}
}
Here is my test:
#Test
public void testPerson() throws Exception {
ObjectMapper objectMapper = new ObjectMapper().registerModule(new GuavaModule());
Person person = new Person();
person.setAge(1);
String jsonString = objectMapper.writeValueAsString(person);
assertEquals(jsonString, "{\"age\":\"1\"}");
}
It's failing with this output:
java.lang.AssertionError: expected [{"age":"1"}] but found [{"age":1}]
I have tried adding a #JsonSerialize annotation to the property, like this:
#JsonSerialize(using = ToStringSerializer.class)
private com.google.common.base.Optional<Integer> age;
But then the test fails like this:
java.lang.AssertionError: expected [{"age":"1"}] but found [{"age":"Optional.of(1)"}]
I've been looking around in com.fasterxml.jackson.datatype.guava.ser.GuavaOptionalSerializer but I can't seem to figure out how to use it correctly, if even if it's meant to be used this way.
What can I do to get this test working?
P.S. If it were up to me, I would remove the Optional altogether, but that's not an option.
I ended up creating my own simple serializer:
public class OptionalStringSerializer extends JsonSerializer<Optional<Object>> {
#Override
public void serialize(Optional<Object> value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
gen.writeString(value.isPresent() ? value.get().toString() : null);
}
}
This is how I specified it:
#JsonSerialize(using = OptionalStringSerializer.class)
private com.google.common.base.Optional<Integer> age;
Related
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?
I have superclass:
#Getter
#Setter
public abstract class AbstractDto {
#NotNull
protected String num;
}
And subclass:
#Getter
#Setter
public class SomeDto extends AbstractDto {
#NotNull
private Long id;
}
When I serialize it, all is ok:
{"num":"5600511164","id":22}
But when I try to deserialize, I get all fields as null in SomeDto.
I use this configuration
#Bean
public ObjectMapper objectMapper (Jackson2ObjectMapperBuilder builder) {
return builder.createXmlMapper(false).build();
}
I think problem is in use superclass, but I don't know how to fix it. Without superclass always work fine.
UPD.
Serialize like this:
protected String marshall(Object message) throws JsonProcessingException {
return this.objectMapper.writeValueAsString(message);
}
Deserialize like this:
protected T unmarshall(byte[] body) throws IOException {
return objectMapper.readValue(body, resolveType());
}
resolveType() return me SomeDto.class
UPD. Solved.
Problem was in body, it's not only {"num":"5600511164","id":22}, it contains another info.
#AllArgsConstructor
#Getter
public enum MemberType {
INTERN("name_intern", 1),
EMPLOYEE("name_employee", 10);
private String name;
private int workingMonth;
}
Here is my enum. I want to convert Enum class to JSON string with some constraint.
I want to MemberType has no dependency with Jackson
I want to convert MemberType.INTERN to {id:INTERN, name:"name_intern", workingMonth:10}.
I have lots of Enums want to convert like above. And Their number of property is different each other.
I want resolve this problem through just one global configuration.
I don't want to use explicit java reflection.
Is there a solution that meets the above constraints?
You can use #JsonFormat annotation like this:
#JsonFormat(shape=JsonFormat.Shape.OBJECT)
public enum MemberType { ... }
or you can use #JsonValue annotation like this:
public enum MemberType {
[...]
#JsonValue
public String getName() {
return name;
}
}
or maybe a CustomSerializer for Enum, you can find more details here.
If you implement JsonSerializer,you can custom serialization.
An example is shown below.
#JsonComponent
public final class MediaTypeJsonComponent {
public static class Serializer extends JsonSerializer<MemberType> {
#Override
public void serialize(MemberType value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeStringField("id", value.name());
gen.writeNumberField("workingMonth", value.getWorkingMonth());
gen.writeStringField("name", value.getName());
gen.writeEndObject();
}
}
//
// If you need,write code.
//public static class Deserializer extends JsonDeserializer<Customer> {
//}
}
Another way is to implement JsonSerialize.
If you want more information, you should refer to:
https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/jackson/JsonComponent.html
https://www.baeldung.com/jackson-custom-serialization
How do I use a custom Serializer with Jackson?
https://www.baeldung.com/jackson-serialize-enums
I've seen many questions around using jackson to serialize/deserialize java objects using builder patter, however, I can't figure out why this code below won't work. I'm using Jackson version 2.5.4
#JsonDeserialize(builder = User.Builder.class)
public class User {
private String name;
private User(Builder builder) {
this.name=builder.name;
}
#JsonPOJOBuilder(buildMethodName = "build")
public static class Builder {
private String name;
public Builder name(String name) {
this.name = name;
return this;
}
public User build() {
return new Learner(this);
}
}
}
Trying to output the string representation always prints an empty list {}
By default the #JsonPOJOBuilderexpects the builder methods to starts with with prefix.
You should override this in the annotation: #JsonPOJOBuilder(withPrefix = "")
You should also mark the name field with the #JsonProperty annotation, or add a getter, or use the JacksonFeatureAutoDetect feature; otherwise Jackson does not see name as a JSON property.
I have a simple controller with method test:
#RequestMapping(produces = "application/json")
#ResponseBody
public HttpEntity<Void> test(Test test) {
return new ResponseEntity<>(HttpStatus.OK);
}
Test class looks like this:
public class Test {
private String name;
private Date date;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getDate() {
return date;
}
#DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
public void setDate(Date date) {
this.date = date;
}
}
And I need default values for fields of Test object. If I had a primitive param, I would be able to use #RequestParam(required = false, defaultValue = "someValue"). But with non-primitive param this approach doesn't seem to work. I see a couple of variants how to deal with it:
Assign values in a constructor. Not very good, because may be I will
need different defaults for different methods.
Write custom DataBinder. Better, but the problem with different
defaults still exists.
Write custom DataBinder and custom annotation with defaults.
Am I missing something and there is a built in feature which can solve my problem?
You can lean on argument resolving, in four easy steps, similiar as you suggested in your third point.
Create an annotation e.g.
#Target(ElementType.PARAMETER)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface TestDefaultValues {
String[] value();
}
Write a resolver e.g.
public class TestArgumentResolver implements HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterAnnotation(TestDefaultValues.class) != null;
}
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
TestDefaultValues attr = parameter.getParameterAnnotation(TestDefaultValues.class);
String[] value = attr.value();
Test test = new Test();
test.setName(value[0]);
test.setDate(new Date(value[1]));
return test;
}
}
register a resolver
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="your.package.TestArgumentResolver"></bean>
</mvc:argument-resolvers>
</mvc:annotation-driven>
use an annotation in your controller method e.g.
public HttpEntity<Void> test(#TestDefaultValues({"foo","11/12/2014"}) Test test) {
instantiating date is just to get the gist of the implementation, obviously you'll use whatever is your idea