#JsonUnwrapped for Custom Serializer - java

I have a relatively complex object which contains a number of fields. I need to serialize one of the fields using a custom serializer, but need to emulate the #JsonUnwrapped functionality.
For simplicity's sake I'll cut this down to two fields:
public class MyClass
{
#JsonProperty("subject")
private final String subject;
#JsonSerialize(using=MySenderSerializer.class)
private final MailActor sender;
}
and my custom serializer class is as follows:
public class MySenderSerializer extends StdSerializer<MailActor>
{
public MySenderSerializer()
{
super(MailActor.class, true);
}
#Override
public void serialize(final MailActor value, final JsonGenerator gen, final SerializerProvider provider) throws IOException
{
gen.writeStringField("from_name", value.getName());
gen.writeStringField("from_email", value.getAddress());
}
}
All of this is fine, except that the output JSON looks like this:
{
...
"subject": "test subject",
"sender": {
"from_name": "test from",
"from_email": "test#test.com"
},
...
}
and I need to unwrap the sender field so that the JSON looks like this:
{
...
"subject": "test subject",
"from_name": "test from",
"from_email": "test#test.com",
...
}
If I was using standard serializers I could use the #JsonUnwrapped annotation to do this, but it doesn't appear to be compatible with custom serializers. How can I obtain the required JSON output without writing a custom serializer for the MyClass object?

I have had to look for an alternative to #JsonUnwrapped too, as it was causing me some unrelated issues with this question.
The solution I have implemented would apply to your case similarly, using #JsonAnyGetter. In your MyClass ignore the attribute that needs the special serialization and instead add it with the JsonAnyGetter:
public class MyClass
{
#JsonProperty("subject")
private final String subject;
#JsonIgnore
private final MailActor sender;
#JsonAnyGetter
public Map<String, Object> serializeActor() {
return sender.serializeToMap();
// Of course, here you could create an empty map
// and add the properties of many different classes.
}
}
Then in the MailActor class you implement that method which will return a map with the properties that you may want.
public Map<String, Object> serializeToMap() {
final Map<String, Object> properties = new ArrayMap<>();
properties.put("from_name", this.getName());
properties.put("from_email", this.getAddress());
return properties;
}
The problem of going this way is deserializing the object, since the #JsonAnySetter doesn't get the map with the JSON in a map the same way you give it with the example above. In your case it's even more difficult, as your class' attributes are final. You'd need to use a #JsonCreator which would have to create all your attributes. Something along the lines of:
#JsonCreator
public MyClass( Map< String, String > json ) {
if (json.containsKey("subject")) {
subject = json.get("subject");
} else {
subject = "";
}
String name, address;
if (json.containsKey("from_name")) {
name = json.get("from_name");
} else {
name = "";
}
if (json.containsKey("from_email")) {
address = json.get("from_email");
} else {
address = "";
}
sender = new MailActor(name, address);
}
It's been a while since you posted the question, hopefully this is helpful in some way to someone coming to look for alternatives.

Well, that is because you have designed it to be that way, when jackson maps an object it will map the inner properties as sub-properties of that object, if you want it to serialize those two fields as if they were members of MyClass instead of MailActor, then declare them as such.
This may point out that your object design may have some small flaws.
I would write a custome serializer for the MyClass object but still, in the long run is not a viable solution.

Related

Mapping object values accounting for multiple Strings

I am receiving the following json from an external rest call.
{
// other keys coming in too, but already mapped
"someData": {
"someContent": {
"item1": "",
"otherItem25": "",
"anotherData34": ""
}
}
}
I wish to map it to an object.
I could do the following which works.
Solution 1
private Map<String, Map<String, String>> someData;
But looking to map it to named objects.
Like following which works too.
Solution 2
class Root {
private someData someData;
}
class SomeData {
private SomeContent someContent;
}
class SomeContent {
#JsonProperty("item1")
private String one;
#JsonProperty("otherItem25")
private String two;
#JsonProperty("anotherData34")
private String three;
}
The issue is with the last bit where I am individually naming the Strings. I don't want to do that cos this is meant to grow
and I could land with about 50 Strings soon. I don't want to create 50 Strings.
Other than the above 2 solutions, is there a better way to map this json to an Object?
Something like this would have been ideal but won't work cos there is nothing to identify as nnerData in the json.
class SomeContent {
private Map<String, String> innerData;
}
Please advice.
P.S: I can't modify the json
Looks like the "ideal case" can be implemented with #JsonCreator annotation and passing the map with "innerData" into constructor.
class TestJson {
#Test
void test() throws Exception {
var json = "{" +
" \"someData\": {" +
" \"someContent\": {" +
" \"item1\": \"hello\"," +
" \"otherItem25\": \"245hb24bt\"," +
" \"anotherData34\": \"b42tb245\"" +
"}}}";
var value = new ObjectMapper().readValue(json, Root.class);
assertThat(value.someData.someContent.innerData.get("item1"))
.isEqualTo("hello");
}
// here we use "public" just to make the code shorter
// and let Jackson bind properties to the public fields
static class Root {
public SomeData someData;
}
static class SomeData {
public SomeContent someContent;
}
static class SomeContent {
public final Map<String, Object> innerData;
#JsonCreator
public SomeContent(Map<String, Object> anyName) {
this.innerData = Map.copyOf(anyName);
// notice that the map "anyName" is mutable
}
}
}
Please, double check if there is no over-engineering here and that is exactly what is required in the solution. Probably, it would be enough to have simply Map type for "someContent" field (and also consider to handle map mutability in this case).
class SomeData {
Map<String, String> someContent;
}
If I'm reading the question correctly, it looks like you want to map most of the object as Java beans, but want the someContent mapped as a Map. Assuming that's the case, you can map the various non-bean properties as normal, and then map someContent as a Map<String, String> on the inner SomeData type.
class Root {
private SomeData someData;
}
class SomeData {
private Map<String, String> someContent;
}
map will be best option in my opinion if your json will be dynamic and will change very often. if you only care about certain properties from any random thing that might be added to response , then just map properties you need to a POJO and use #JsonIgnoreProperty(ignore=unknown) annotation to ignore additional things that will come in response that you arent even interested.

Deserialize String as List of Strings

I have a POJO that contains the following attributes
public class Example {
#JsonProperty("inputFoo")
private String foo
#JsonProperty("inputBar")
private String bar
#JsonProperty("inputBaz")
#JsonDeserialize(using = MyDeserializer.class)
private Set<String> baz
}
The JSON that I am working with to represent this data currently represents the baz attribute as a single string:
{"inputFoo":"a", "inputBar":"b", "inputBaz":"c"}
I am using the Jackson ObjectMapper to attempt to convert the JSON to my POJO. I know that the input baz String from the JSON wont map cleanly to the Set that I am trying to represent it as, so I defined a custom Deserializer:
public class MyDeserializer extends StdDeserializer<Set<String>> {
public MyDeserializer(){}
public MyDeserializer(Class<?> vc) {
super(vc);
}
public Set<String> deserialize(JsonParser p, DeserializationContext cxt) throws IOException, JsonProcessingException {
String input = p.readValueAs(String.class);
Set<String> output = new HashSet<>();
if(input != null) {
output.add(input);
}
return output;
}
}
I am getting an IllegalArgumentException referencing the "inputBaz" attribute, which I can provide details on. Does anyone see any obvious issue with my deserializer implementation? Thanks
You do not need to implement custom deserialiser, use ACCEPT_SINGLE_VALUE_AS_ARRAY feature. It works for sets as well:
Feature that determines whether it is acceptable to coerce non-array
(in JSON) values to work with Java collection (arrays,
java.util.Collection) types. If enabled, collection deserializers will
try to handle non-array values as if they had "implicit" surrounding
JSON array. This feature is meant to be used for
compatibility/interoperability reasons, to work with packages (such as
XML-to-JSON converters) that leave out JSON array in cases where there
is just a single element in array. Feature is disabled by default.
See also:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of java.util.ArrayList out of START_OBJECT token
Replace the 2 constructors with this no-arg constructor:
public MyDeserializer() {
super(TypeFactory.defaultInstance().constructCollectionType(Set.class, String.class));
}
ACCEPT_SINGLE_VALUE_AS_ARRAY as suggested is a good option.
Maybe your actual problem is more complicated but if not you could also try #JsonCreator instead of custom deserializer. Like:
public class Example {
#JsonCreator
public Example(#JsonProperty("inputFoo") String foo,
#JsonProperty("inputBar") String bar,
#JsonProperty("inputBaz") String strBaz) {
this.foo = foo;
this.bar = bar;
this.baz = new HashSet<>();
baz.add(strBaz);
}
private String foo;
private String bar;
private Set<String> baz;
}
Just to show that in more general case you might avoid implementing custom deserializer with #JsonCreator also but still make some simple conversions.

Is it possible to de/serialize map itself polymorphic in jackson?

I've searched a lot and only find questions about polymorphic deserialization on the content inside a map. Is it possible to polymorphic deserializing the map itself?
For example, I have a Book class contains a Map as a member variable.
public class Book {
#JsonProperty
private Map<String, Object> reviews;
#JsonCreator
public Book(Map<String, Object> map) {
this.reviews = map;
}
}
Another class have a list of Book class.
public class Shelf {
#JsonProperty
private List<Book> books = new LinkedList<>();
public void setBooks(List<Book> books) {
this.books = books;
}
public List<Book> getBooks() {
return this.books;
}
}
And a test class. One book's review map is a Hashtable and another book's review map is a HashMap.
public class Test {
private Shelf shelf;
#BeforeClass
public void init() {
Map<String, Object> review1 = new Hashtable<>(); // Hashtable here
review1.put("test1", "review1");
Map<String, Object> review2 = new HashMap<>(); // HashMap here
review2.put("test2", "review2");
List<Book> books = new LinkedList<>();
books.add(new Book(review1));
books.add(new Book(review2));
shelf = new Shelf();
shelf.setBooks(books);
}
#Test
public void test() throws IOException{
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
// mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
String json = mapper.writeValueAsString(shelf);
System.out.println(json);
Shelf sh = mapper.readValue(json, Shelf.class);
for (Book b : sh.getBooks()) {
System.out.println(b.getReviews().getClass());
}
}
}
The test output
{
"name" : "TestShelf",
"books" : [ {
"reviews" : {
"test1" : "review1"
}
}, {
"reviews" : {
"test2" : "review2"
}
} ]
}
class java.util.LinkedHashMap
class java.util.LinkedHashMap
The serialization works fine. But after deserialization, both review1 and review2 are LinkedHashMap. I want review1 and review2 to be their actual types which are Hashtable to review1 and HashMap to review2. Is there any way to achieve this?
I don't want to use mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); because it will add the type info for all json properties in the json message. And if there is any better way to do it I don't want to use customized deserializer either. Thanks in advance.
I posted the question on Jackson user forum and they suggest to customized the TypeResolverBuilder and set it in the ObjectMapper instance.
ObjectMapper.setDefaultTyping(...)
My customized TypeResolverBuilder is below and it solved my problem.
public class MapTypeIdResolverBuilder extends StdTypeResolverBuilder {
public MapTypeIdResolverBuilder() {
}
#Override
public TypeDeserializer buildTypeDeserializer(DeserializationConfig config,
JavaType baseType, Collection<NamedType> subtypes) {
return useForType(baseType) ? super.buildTypeDeserializer(config, baseType, subtypes) : null;
}
#Override
public TypeSerializer buildTypeSerializer(SerializationConfig config,
JavaType baseType, Collection<namedtype> subtypes) {
return useForType(baseType) ? super.buildTypeSerializer(config, baseType, subtypes) : null;
}
/**
* Method called to check if the default type handler should be
* used for given type.
* Note: "natural types" (String, Boolean, Integer, Double) will never
* use typing; that is both due to them being concrete and final,
* and since actual serializers and deserializers will also ignore any
* attempts to enforce typing.
*/
public boolean useForType(JavaType t) {
return t.isMapLikeType() || t.isJavaLangObject();
}
}
This solution requires both server side and client side to use the customized TypeResolverBuilder. I know it is not ideal, but it is the best solution I found so far. The details of the solution can be found in this post on my blog.
The readValue call has no idea where the input JSON came from. It doesn't know that it was generated from a Hashtable or a HashMap or a TreeMap or any other type of Map. All it has to work with is the target type, Shelf, and its nested Book. The only thing Jackson can introspect from Book is that it has a field of type Map.
Map is an interface. Since you can't instantiate an interface, Jackson has to make a decision on the implementation type of Map that it wants to use. By default, it uses LinkedHashMap. You can change the default by following the solution posted here.
An alternative is to declare the field with the concrete type you want
private HashMap<String, Object> reviews;
Now Jackson knows to deserialize the JSON into a HashMap. Obviously, this will only work with a single type.
The actual solution is for you not to care about the actual implementation class. You decided it was going to be a Map. You shouldn't care what implementation it uses under the covers. Use the power of polymorphism.
(Note that the use of Hashtable has long been discouraged.)

Jackson Deserialize to POJO Based On Object Properties

Is it possible to deserialize JSON using Jackson into one of two types based on the content of the JSON?
For example, I have the following Java (technically Groovy, but that's not important) interfaces and classes:
interface Id {
Thing toThing()
}
class NaturalId implements Id {
final String packageId
final String thingId
Thing toThing() {
new PackageIdentifiedThing(packageId, thingId)
}
}
class AlternateId implements Id {
final String key
Thing toThing() {
new AlternatelyIdentifiedThing(key)
}
}
The JSON I will receive will look like either of the following:
This JSON should map to NaturalId {"packageId": "SomePackage", "thingId": "SomeEntity"}
This JSON should map to AlternateId {"key": "SomeUniqueKey"}
Does anyone know how I can accomplish this with Jackson 2.x WITHOUT including type id's?
Are these the only two classes that implement Id? If so, you could write an IdDeserializer class and put #JsonDeserialize(using = IdDeserializer.class) on the Id interface, and the deserializer would look at the JSON and determine which object to deserialize into.
EDIT: The JsonParser is streaming so it should look something like this:
public Id deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
ObjectNode node = jp.readValueAsTree();
Class<? extends Id> concreteType = determineConcreteType(node); //Implement
return jp.getCodec().treeToValue(node, concreteType);
}
Annotate your methods with #JsonIgnore
#JsonIgnore
Thing toThing() {
new PackageIdentifiedThing(packageId, thingId)
}
With Jackson2, you can easily marshall to different classes using generics:
private <T> T json2Object(String jsonString, String type, Class<T> clazz) {
JsonNode jsonObjectNode = getChildNode(jsonString, type);
T typeObject = null;
try {
typeObject = jacksonMapper.treeToValue(jsonObjectNode, clazz);
} catch (JsonProcessingException jsonProcessingException) {
LOGGER.severe(jsonProcessingException);
}
return typeObject;
}

Json Jackson Mapper Generic Map Type issue

This is my firs titme dealing with Type Maps and everytime i try to map the node to my Actual Type Object which has a custom property key as FooType with a Set<Integer> values. Here is how my Object looks like
public class Foo {
private String some;
Map<FooTypes,Set<Integer>> foos;
public Map<FooTypes, Set<Integer>> getFoos() {
return foos;
}
public void setFoos(Map<FooTypes, Set<Integer>> map) {
this.foos = map;
}
public String getSome() {
return some;
}
public void setSome(String some) {
this.some = some;
}
}
public class FooTypes {
private String name;
private String id;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
Now everytime i try to use the mapper to read the value :-
List <Foo> response = mapper.readValue("/home/foo/foo.json",List.class);
I get an error stating that :-
Can not find a (Map) Key deserializer for type [simple type, class cruft.FooTypes]
Can someone tell me on how can i fix this problem ? Thank you.
Json Output:-
{"foos":{"FooTypes [id=1, name=Test Foo]":[1,2,3]},"some":hello},{"foos":{"FooTypes [id=2, name=Another foo]":[5,6,7]}}
It's a bit hard to help you since we don't have the Json structure you want to deserialize, but the problem here is Jackson has no idea how to deserialize your class when it is used as a map key. All the information Jackson as is a simple String, and your class provide no way of creating it with only a string.
There's 3 way to achieve this:
Add a single string argument constructor to your FooType class
Add a single string argument factory method (static) and annotate it with #JsonCreator
Use a custom KeyDeserializer and annotate the foos field with #JsonDeserialize(keyUsing=YourDeserializer.class)
IMHO the static factory method is the cleaner way.
Since you have non-primitive type as a map key (FooTypes) you'll need to write your own custom deserializer as described in Jackson wiki, because Jackson can't simply convert string value "FooTypes [id=1, name=Test Foo]" (which seems to be a result of FooTypes.toString()) into FooTypes instance.
On my opinion, serializing map with non-string keys is not really a good practice. JSON notation doesn't support anything but strings for map keys (as specified here). A better approach, i think, would be to reorganize your data structure before serialization and after deserialization.

Categories

Resources