1

I have a JAX-RS REST endpoint of PUT type and I am supposed to pass a Map to this API.

@PUT
@Path("/some/path")
@Consumes({ MediaType.TEXT_PLAIN, MediaType.APPLICATION_XML,
        MediaType.TEXT_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response updatePerson(HashMap<Person, Person> map) {

//some code here
}

I generated JSON for Person class but I am not able to pass it as a JSON input to this API. I am using Postman client and it says Syntax error while I try to pass the JSON input as key value pair. JSON generated for Person looks something like below

  {"name":"abc","weight":100.0,"id":"123"}

I need to pass this as key value pair as a map. Something like

 {
   {"name":"abc","weight":100.0,"id":"123"} : 
   {"name":"def","weight":200.0,"id":"123"}
 }

Any pointers how do I do this?

deepankardixit90
  • 351
  • 4
  • 19
  • As far as I can tell you can't have objects as keys, they need to be strings. Can you work your way around that by using a serialised person as the key? Or restructure the data? – JussiV Feb 28 '19 at 13:52
  • Use a unique attribute of Person as key instead? Like `id` unless that is not unique like in your example? – Joakim Danielson Feb 28 '19 at 13:56
  • Unfortunately I can't change that. Is there a way to pass JSON input to this map? – deepankardixit90 Feb 28 '19 at 14:07
  • I didn't look to carefully at the annotations earlier but for @Consumes you have PLAIN_TEXT in the list. – Joakim Danielson Feb 28 '19 at 15:10
  • @raghav `{ {"name":"abc","weight":100.0,"id":"123"} : {"name":"def","weight":200.0,"id":"123"} }` this is invalid `JSON` payload. Check with [online validator](https://jsonformatter.curiousconcept.com/). You need to parse it manually. In controller change `Map` to `String` and deserialise it manually by defining some kind of custome deserialiser. – Michał Ziober Feb 28 '19 at 15:43
  • @MichałZiober Hi Michel. Thanks for reply. Yes I know thats an invalid payload as Postman itself is giving syntax errors. I was just trying to give an idea of what I was expecting. May be I have no other option than to send it as String and deserialize back there. – deepankardixit90 Feb 28 '19 at 20:24
  • @raghav, can you change controller implementation? Can you change `Map` for something else? – Michał Ziober Feb 28 '19 at 22:11
  • 1
    @MichałZiober I don't recall much about this scenario now but your comments certainly helped me back then. – deepankardixit90 Feb 18 '21 at 14:17

1 Answers1

0

Generally it looks like a bad idea to create Map like this. JSON Object can be converted to Java Map where key is String and value is any Object: can be another Map, array, POJO or simple types. So, generally your JSON should look like:

{
    "key" : { .. complex nested object .. }
}

Theres is no other option. If you want to have in Java mapping POJO -> POJO you need to instruct deserialiser how to convert JSON-String-key to an object. There is no other option. I will try to explain this process using Jackson library because it is most used in RESTful Web Services. Let's define Person class which fits to your JSON payload.

class Person {

    private String name;
    private double weight;
    private int id;

    public Person() {
    }

    public Person(String value) {
        String[] values = value.split(",");
        name = values[0];
        weight = Double.valueOf(values[1]);
        id = Integer.valueOf(values[2]);
    }

    public Person(String name, double weight, int id) {
        this.name = name;
        this.weight = weight;
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getWeight() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return name + "," + weight + "," + id;
    }
}

Because it is used in Map as key we need to implement hashCode and equals methods. Except public Person(String value) constructor and toString method everything else looks pretty normal. Now, let's take a look on this constructor and toString method. They are in correlation: toString builds String from the Person instance and constructor builds Person from String. We can call first transformation as serialisation and second as deserialisation of our key in Map serialisation and deserialisation. (Whether these two are well implemented this is another story. I just want to show an idea behind it. Before using on production should be improved)

Let's use this knowledge and Jackson features to serialise and deserialise Map<Person, Person>:

import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.MapType;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class JsonApp {

    public static void main(String[] args) throws Exception {
        // register deserializer for Person as keys.
        SimpleModule module = new SimpleModule();
        module.addKeyDeserializer(Person.class, new PersonKeyDeserializer());

        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(module);
        mapper.enable(SerializationFeature.INDENT_OUTPUT);

        // Create example Map
        Person key = new Person("Rick", 80.5, 1);
        Person value = new Person("Morty", 40.1, 2);
        Map<Person, Person> personMap = new HashMap<>();
        personMap.put(key, value);

        // Serialise Map to JSON
        String json = mapper.writeValueAsString(personMap);
        System.out.println(json);

        // Deserialise it back to `Object`
        MapType mapType = mapper.getTypeFactory().constructMapType(HashMap.class, Person.class, Person.class);
        System.out.println(mapper.readValue(json, mapType).toString());
    }
}

class PersonKeyDeserializer extends KeyDeserializer {

    @Override
    public Object deserializeKey(String key, DeserializationContext ctxt) {
        return new Person(key);
    }
}

Above code prints as first JSON:

{
  "Rick,80.5,1" : {
    "name" : "Morty",
    "weight" : 40.1,
    "id" : 2
  }
}

As you can see, Person's toString method was used to generate JSON key. Normal serialisation proces serialised Person to JSON object. As second below text is printed:

{Rick,80.5,1=Morty,40.1,2}

This is a default representation of Map and it's keys and values. Because both are Person object it's toString method is invoked.

As you can see there is an option to send JSON as Map<Person, Person> but key should be somehow represented. You need to take a look on Person class implementation. Maybe you will find some similarities to my example. If no, maybe it was configured somehow. First of all try to send in PostMan:

{
   "123" : {"name":"def","weight":200.0,"id":"123"}
}

Or:

{
   "{\"name\":\"abc\",\"weight\":100.0,\"id\":\"123\"}":{
      "name":"def",
      "weight":200.0,
      "id":"123"
   }
}

Maybe it will work.

See also:

Michał Ziober
  • 37,175
  • 18
  • 99
  • 146