Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JsonMappingException: No suitable constructor found for type -- for an external object

I have an object called GeoJsonPoint from spring framework, and it can't get deserialized by jackson mapper in my integration test. Additionally, I can't add a dummy constructor because it is an external object. So I am stuck. This is my main entity;

@Document(collection = "foodTrucks")
@JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY)
public class FoodTruckEntity {

    @Id
    private ObjectId id;

    private String applicant;
    private Status status;
    private String[] foodItems;
    private Double longitude;
    private Double latitude;
    private GeoJsonPoint geoJsonPoint;

    public FoodTruckEntity() {};

    // getters and setters
}

And the test

@Test
public void test() {
    ClientConfig clientConfig = new DefaultClientConfig();
    clientConfig.getFeatures().put(JSONConfiguration.FEATURE_POJO_MAPPING, Boolean.TRUE);
    Client client = Client.create(clientConfig);

    String getNearFoodTrucksUrl = "http://localhost:8080/food-truck/near-locations/longitude/-122.398658184604/latitude/37.7901490737255/findAll";
    WebResource webResource = client.resource(getNearFoodTrucksUrl);
    ClientResponse response = webResource.get(ClientResponse.class);
    GeoResults<FoodTruckEntity> geoResults = webResource.get(new GenericType<GeoResults<FoodTruckEntity>>(){});

    if (response.getStatus() != 200) {
        throw new WebApplicationException();
    }
}

And the error I get;

com.sun.jersey.api.client.ClientHandlerException: org.codehaus.jackson.map.JsonMappingException: No suitable constructor found for type [simple type, class org.springframework.data.geo.GeoResults<entity.FoodTruckEntity>]: can not instantiate from JSON object (need to add/enable type information?)
 at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@107ed6fc; line: 1, column: 2]
    at com.sun.jersey.api.client.ClientResponse.getEntity(ClientResponse.java:644)
    at com.sun.jersey.api.client.ClientResponse.getEntity(ClientResponse.java:604)

EDIT: This is the dependencies for jersey I have

<!-- JERSEY JSON -->
<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-client</artifactId>
    <version>1.18.1</version>
</dependency>

<dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>1.18.1</version>
</dependency>
<!-- JERSEY JSON -->

EDIT_2: Response as a String looks like this,

{
   "averageDistance":{
      "value":0.0,
      "metric":"MILES",
      "normalizedValue":0.0,
      "unit":"mi"
   },
   "content":[
      {
         "content":{
            "id":{
               "timestamp":1429498845,
               "machineIdentifier":11487078,
               "processIdentifier":1432,
               "counter":9275496,
               "time":1429498845000,
               "date":1429498845000,
               "timeSecond":1429498845
            },
            "applicant":"Cupkates Bakery, LLC",
            "facilityType":"Truck",
            "status":"APPROVED",
            "foodItems":[
               "Cupcakes"
            ],
            "longitude":-122.398658184604,
            "latitude":37.7901490737255,
            "geoJsonPoint":{
               "x":-122.398658184604,
               "y":37.7901490737255,
               "type":"Point",
               "coordinates":[
                  -122.398658184604,
                  37.7901490737255
               ]
            }
         },
         "distance":{
            "value":0.0,
            "metric":"MILES",
            "normalizedValue":0.0,
            "unit":"mi"
         }
      }
   ]
}
like image 259
quartaela Avatar asked Apr 19 '15 23:04

quartaela


1 Answers

So if you look at all the classes from the org.springframework.data.geo, you will notice that pretty much all the classes don't have a no-arg constructor, which the default behavior of the ObjectMapper needs to deserialize POJOs from JSON.

One way to get around this with third-party APIs, is to make use of Jackson Mixins. If you look at the GeoModule, this is a module you can register with the ObjectMapper, that includes some Mixins

mapper.registerModule(new GeoModule());

If you look at the org.springframework.data.mongodb.core.geo, you will another module GeoJsonModule that has some included Mixins also. This module should take care of the GeoJsonPoint.

But the main problem for your use case, is (if you look back at the GeoModule) is that there is no Mixin for the GeoResult or GeoResults, which you need to parse the JSON.

I made a module to take care of the GeoResult but the GeoResults doesn't work at the moment.

public class GeoModuleExt extends SimpleModule {

    public GeoModuleExt() {
        super("Mixins", new Version(1, 0, 0, null));
        setMixInAnnotation(GeoResult.class, GeoResultMixin.class);
        setMixInAnnotation(GeoResults.class, GeoResultsMixin.class);
    }
    
    static abstract class GeoResultMixin {
        GeoResultMixin(@JsonProperty("content") Object content, 
                       @JsonProperty("distance") Distance distance) {    
        }
    }
    
    static abstract class GeoResultsMixin {
        GeoResultsMixin(@JsonProperty("results")List<GeoResult> results) {
           
        }   
    }
}

You can play around with it. I don't have time right now to work on it (hence the half-@$$ solution), but when I get some time, if you haven't figured it out, I'll see what I can do.

As a test, you can use the ObjectMapper in a standalone, to make sure it works first

public class Test {

    public static void main(String[] args) throws Exception {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new GeoJsonModule());
        mapper.registerModule(new GeoModule());
        // Our custom module
        mapper.registerModule(new GeoModuleExt());

        // Create FoodTruckEntity - 
        GeoJsonPoint geoJsonPoint = new GeoJsonPoint(10, 10);
        FoodTruckEntity foodTruck = new FoodTruckEntity();
        // set all properties fro foodTruck

        // Create GeoResult
        GeoResult<FoodTruckEntity> geoResult
                = new GeoResult<FoodTruckEntity>(foodTruck, new Distance(10,
                                                 Metrics.KILOMETERS));
        
        // Serialize geoResult
        String geoResultString = mapper.writeValueAsString(geoResult);
        System.out.println(geoResultString);
        
        JavaType type = mapper.getTypeFactory().constructParametricType(
                GeoResult.class, FoodTruckEntity.class);
        
        // Deserialize geoResultString
        GeoResult<FoodTruckEntity> parsedGeoResult 
                = mapper.readValue(geoResultString, type);
        System.out.println(parsedGeoResult.getDistance());
        System.out.println(parsedGeoResult.getContent().getApplicant());
        
        // Up to this point everything is fine. It's the deserialization of
        // `GeoResults` thats a problem.
        
        /*
        List<GeoResult> results = new ArrayList<GeoResult>();
        results.add(geoResult);
        results.add(geoResult);
        GeoResults geoResults = new GeoResults(results);
        
        String resultsString = mapper.writeValueAsString(geoResults);
        System.out.println(resultsString);
        
        
        JavaType resultType = mapper.getTypeFactory().constructParametricType(
                                       GeoResults.class, FoodTruckEntity.class);
        
        GeoResults<FoodTruckEntity> parsedGeoResults 
                            = mapper.readValue(resultsString, resultType);
        for (GeoResult<FoodTruckEntity> gr: parsedGeoResults) {
            System.out.println(gr.getContent().getGeoJsonPoint());
        }*/
    }
}

When you get the test to work, you can register the ObjectMapper with Jersey like

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new GeoJsonModule());
mapper.registerModule(new GeoModule());
// Our custom module
mapper.registerModule(new GeoModuleExt());

ClientConfig config = new DefaultClientConfig();
config.getSingletons().add(new JacksonJsonProvider(mapper));
Client client = Client.create(config);

UPDATE

So after some playing around, I was able to get it to work with this Mixin for the GeoResults. Just update the above GeoModuleExt

static abstract class GeoResultsMixin {
    GeoResultsMixin(@JsonProperty("results") List<GeoResult> results, 
                    @JsonProperty("averageDistance") Distance averageDistance) {
    }
    
    @JsonProperty("results")
    abstract List<GeoResult> getContent(); 
}

It works as expected with the above test. Haven't tested yet with Jersey, but if it works with the ObjectMapper, it shouldn't be a problem with Jersey, as long as we have configured the Jackson provider to use the mapper.

like image 98
Paul Samsotha Avatar answered Oct 03 '22 22:10

Paul Samsotha