Exclude or specify data type model packages. Wrong array items type in generic collection properties

2,052 views
Skip to first unread message

Andreas Klein

unread,
Jun 12, 2015, 3:04:04 AM6/12/15
to swagger-sw...@googlegroups.com
We are seeing some weird behaviour from our swagger implementation after migrating from 1.3 to 1.5.
(We moved from com.wordnik:swagger-jersey-jaxrs_2.10:1.3.12 to io.swagger:swagger-jersey-jaxrs:1.5.0)

The problem is that Swagger appears to be guessing the wrong element class for generic collection type properties in our API model, if there is another class with the same Class.simpleName in the classpath.
Best to explain with an example. Here are some simplified/modified classes from our application:

These are two of our API model classes

package com.livngds.gds.api.model;

@javax.xml.bind.annotation.XmlRootElement(name="cart")
@javax.xml.bind.annotation.XmlAccessorType(javax.xml.bind.annotation.XmlAccessType.FIELD)
@org.codehaus.jackson.map.annotate.JsonRootName("cart")
@io.swagger.annotations.ApiModel
public class Cart {

@javax.xml.bind.annotation.XmlElement
private Long id;

@javax.xml.bind.annotation.XmlElementWarapper(name="cartItems", required=true)
@javax.xml.bind.annotation.XmlElement(name="cartItem")
private java.util.List<com.livngds.gds.api.model.CartItem> cartItems;
//more fields, getters & setters
}

package com.livngds.gds.api.model;

@javax.xml.bind.annotation.XmlRootElement(name="cartItem")
@javax.xml.bind.annotation.XmlAccessorType(javax.xml.bind.annotation.XmlAccessType.FIELD)
@org.codehaus.jackson.map.annotate.JsonRootName("cartItem")
@io.swagger.annotations.ApiModel
public class CartItem {

//this is the API model class we want Swagger to use

@javax.xml.bind.annotation.XmlElement
private Long id;
@javax.xml.bind.annotation.XmlElement
private String productName; //just an exemplary field we want and need in the API model
//more fields, getters & setters
}


We also have two equally named Hibernate entity classes in our application's persistence module

package com.livngds.gds.persistence.sales.master;

@javax.persistence.Entity
//more JPA/Hibernate annotations
public class Cart {

//This is our Hibernate entity class. It is never used as an API model type. Neither as parameter or return type in our resources, nor as a property anywhere else in our API model

private Long id;
private List<com.livngds.gds.persistence.sales.master.CartItem> cartItems;
}

package com.livngds.gds.persistence.sales.master;

@javax.persistence.Entity
//more JPA/Hibernate annotations
public class CartItem {

//This is our Hibernate entity class. It is never used as an API model type. Neither as parameter or return type in our resources, nor as a property anywhere else in our API model

private Long id;
private String productName;
private String secretNotIntendedForAPI;
}

Finally this is a reduced version of the related JAX-RS resource class dealing with our carts

package com.livngds.gds.api.server.resource; //This is the package we let Swagger scan for resources

@javax.ws.rs.Path("/carts")
@io.swagger.annotations.Api(value = "carts")
public class CartsResource {

@javax.ws.rs.GET
@javax.ws.rs.Produces({javax.ws.rs.core.MediaType.APPLICATION_JSON, javax.ws.rs.core.MediaType.APPLICATION_XML})
@javax.annotation.security.RolesAllowed(com.livngds.gds.persistence.master.enums.ApiUserRoleConstantString.READ)
@javax.ws.rs.Path("{cartId:\\d+}")
@io.swagger.annotations.ApiOperation(value = "Retrieves a specific cart.", response = com.livngds.gds.api.model.Cart.class)
@io.swagger.annotations.ApiResponses(value = {
     @io.swagger.annotations.ApiResponse(code = 400, message = "Invalid id supplied.", response=com.livngds.gds.api.exceptions.WebApplicationExceptionBody.class),
     @io.swagger.annotations.ApiResponse(code = 401, message = "Missing API authorisation.", response=com.livngds.gds.api.exceptions.WebApplicationExceptionBody.class),
     @io.swagger.annotations.ApiResponse(code = 404, message = "Cart not found.", response=com.livngds.gds.api.exceptions.WebApplicationExceptionBody.class)
 })
public com.livngds.gds.api.model.Cart getCart(@io.swagger.annotations.ApiParam(value="Cart ID") @javax.ws.rs.PathParam("cartId") long cartId) {
com.livngds.gds.api.model.Cart apiCartToReturn;
//We retrieve a com.livngds.gds.persistence.sales.master.Cart from our database using the specified cartId and convert it to a com.livngds.gds.api.model.Cart using only the fields that are intended for the API
//com.livngds.gds.persistence.sales.master.CartItem instances likewise are translated into com.livngds.gds.api.model.CartItem and so on
return apiCartToReturn;
}
}


Our problem: Sadly Swagger doesn't appear to resolve the generics element types as required and our swagger.json for instance contains a type definition for CartItem, which is not based on our API model class CartItem, but the Hibernate entity class. It contains properties, which are not part of com.livngds.gds.api.model.CartItem (in this reduced example that would be the field secretNotIntendedForAPI, only found in com.livngds.gds.persistence.sales.master.CartItem):

{
"swagger": "2.0",
"info": {
"version": "1.0",
"title": "Development GDS API Documentation"
},
"basePath": "/livngds-web/api",
"tags": [{
"name": "carts"
}],
"schemes": ["https"],
"paths": {
"/carts/{cartId}": {
"get": {
"tags": ["carts"],
"summary": "Retrieves a specific cart.",
"description": "",
"operationId": "getCart",
"produces": ["application/json",
"application/xml"],
"parameters": [{
"name": "cartId",
"in": "path",
"description": "Cart ID",
"required": true,
"type": "integer",
"pattern": "\\d+",
"format": "int64"
}],
"responses": {
"404": {
"description": "Cart not found.",
"schema": {
"$ref": "#/definitions/WebApplicationExceptionBody"
}
},
"200": {
"description": "successful operation",
"schema": {
"$ref": "#/definitions/Cart"
}
},
"401": {
"description": "Missing API authorisation.",
"schema": {
"$ref": "#/definitions/WebApplicationExceptionBody"
}
},
"400": {
"description": "Invalid id supplied.",
"schema": {
"$ref": "#/definitions/WebApplicationExceptionBody"
}
}
}
}
}
},
"definitions": {
"CartItem": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"productName": {
"type": "string"
},
"secretNotIntendedForAPI": {
"type": "string"
}
}
},
"Cart": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"cartItems": {
"type": "array",
"xml": {
"name": "cartItem",
"wrapped": true
},
"items": {
"$ref": "#/definitions/CartItem"
}
}
},
"xml": {
"name": "cart"
}
},
"WebApplicationExceptionBody": {
"type": "object",
"properties": {
"status": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
}
}
}
}
}

It appears as if on the search for a matching list item type, to serve as array.items property in the generated JSON, swagger grabs the first class it can find in our class path, that matches the SimpleName of the generic type.

We have tried a number of ways to annotate the data type on our generic collection com.livngds.gds.api.model.Cart.cartItems:

@ApiModelProperty(dataType="com.livngds.gds.api.model.CartItem")

This causes the correct CartItem class to be used in definitions, but of course it does not map the List to a JSON array, but a single wrapped object

@ApiModelProperty(dataType="[com.livngds.gds.api.model.CartItem]")
@ApiModelProperty(dataType="java.util.List[com.livngds.gds.api.model.CartItem]")
@ApiModelProperty(dataType="java.util.List<com.livngds.gds.api.model.CartItem>")
@ApiModelProperty(dataType="array[com.livngds.gds.api.model.CartItem]")

These attempts all cause a ClassNotFoundException.

Is there any other way to define the inner data type of collections? Or is there otherwise any way to prevent Swagger from using classes in certain packages (i.e. our persistence) or limit its search for model classes to specific packages?
If this is impractical, would it make sense to assume classes actually annotated with @ApiModel should take preference over those that are not?
Or, are we getting something quite fundamentally wrong here ;) ?

Any help would be greatly appreciated. Many thanks in advance!

Andreas Klein

unread,
Jun 12, 2015, 3:51:41 AM6/12/15
to swagger-sw...@googlegroups.com
Just as a follow-up, annotating the List<CartItem> property like this, also doesn't help.

@XmlElementWrapper(name="cartItems", required=true)
@XmlElement(name="cartItem", type=com.livngds.gds.api.model.CartItem.class)
private List<CartItem> cartItems;

Swagger still uses the wrong CartItem class.

Matt Traynham

unread,
Jun 13, 2015, 2:15:05 AM6/13/15
to swagger-sw...@googlegroups.com
Seeing this as well.  I have an even bigger problem with this as well, as one of the model objects breaks the swagger-ui because it doesn't produce a $ref for it's underlying collection.

Andreas Klein

unread,
Jun 13, 2015, 4:08:43 AM6/13/15
to swagger-sw...@googlegroups.com
Hi Matt,

I was having that UI issue as well and from a similar sounding problem learnt that it is because one (or in my case 8) of the arrays in swagger.json had no items type declared at all. All of these were for collections in the wrongfully digested Hibernate entity classes.

I managed to do a quick and dirty patch in the swagger-ui.js to get the UI working until I can figure out the underlying issue with the wrong type definitions.
You can modify swagger-ui.js as follows. In the current v2.1.0 go to line 2347 and change

Resolver.prototype.resolveTo = function (root, property, resolutionTable, location) {
  var ref = property.$ref;

  if (ref) {
    if(ref.indexOf('#') >= 0) {
      location = ref.split('#')[1];
    }
    resolutionTable.push({
      obj: property, resolveAs: 'ref', root: root, key: ref, location: location
    });
  } else if (property.type === 'array') {
    var items = property.items;
    this.resolveTo(root, items, resolutionTable, location);
  }
};

to

Resolver.prototype.resolveTo = function (root, property, resolutionTable, location) {
  var ref = property.$ref;

  if (ref) {
    if(ref.indexOf('#') >= 0) {
      location = ref.split('#')[1];
    }
    resolutionTable.push({
      obj: property, resolveAs: 'ref', root: root, key: ref, location: location
    });
  } else if (property.type === 'array') {
    var items = property.items;
    
    if (typeof items != 'undefined') {
    this.resolveTo(root, items, resolutionTable, location);
    } else {
    this.resolveTo(root, 'string', resolutionTable, location);
    }
  }
};

It's dirty, but at least it'll give you a GUI at all until we can figure out why we're getting all these non API related classes in the API model.
Hope that helps.

Andreas Klein

unread,
Jun 14, 2015, 9:22:06 PM6/14/15
to swagger-sw...@googlegroups.com
After enabling swagger's logger on debug level, I finally found that we had actually wrongly configured an @ApiOperation with one of our persistence entity classes as response. Subsequently we had all kinds of persistence classes trickle into our model definitions.

So if you are experiencing similar issues and like us weren't able to pinpoint  the culprit among gazillions of API operation methods, I recommend doing the same: set swaggers loggers to debug (in Log4j that's log4j.logger.io.swagger), access your swagger.json then search the log output for the offending classes' package names.

Now we "only" need to figure out how to prevent swagger from including java.lang.Throwable. The documented approach would be using OverrideConverter, but that class seems to be gone :(

Ron Ratovsky

unread,
Jun 15, 2015, 2:44:14 AM6/15/15
to swagger-sw...@googlegroups.com
Hi Andreas,

Just wanted to make sure - did you resolve all the non-conversion-related issues you had? I've replied the conversion in the other thread.
Also, for Throwable, feel free to open an issue about it on the project (would be great if you could include a sample class). We may want to ignore it by default.

--
You received this message because you are subscribed to the Google Groups "Swagger" group.
To unsubscribe from this group and stop receiving emails from it, send an email to swagger-swaggers...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
-----------------------------------------
http://swagger.io
https://twitter.com/SwaggerApi
-----------------------------------------

Andreas Klein

unread,
Jun 15, 2015, 3:41:30 AM6/15/15
to swagger-sw...@googlegroups.com
Hi Ron,

Thanks, we did. As I said we had the wrong return type defined in just a single @ApiOperation annotation, basically saying @ApiOperation(...response = hibernate.model.Cancellation.class) instead of @ApiOperation(...response = api.model.Cancellation.class).
Subsequently swagger was loading all kinds of other persistence model classes referenced by Cancellation into its model definitions and the whole thing fell over. I simply could not find that bug for the life of me until going through the debug logs.
Fixed that one annotation and there were no more persistence entities in the API model.

I'll respond to the other issues in that thread.

Thanks again, Andreas


On Monday, June 15, 2015 at 4:44:14 PM UTC+10, Ron wrote:
Hi Andreas,

Just wanted to make sure - did you resolve all the non-conversion-related issues you had? I've replied the conversion in the other thread.
Also, for Throwable, feel free to open an issue about it on the project (would be great if you could include a sample class). We may want to ignore it by default.
To unsubscribe from this group and stop receiving emails from it, send an email to swagger-swaggersocket+unsub...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

Matt Traynham

unread,
Jun 30, 2015, 4:53:11 PM6/30/15
to swagger-sw...@googlegroups.com
I sort of resolved my issue.  One of the POST body parameters in our Rest API is a Avro generated model.  Avro model's have a method called getSchema():org.apache.avro.Schema, and the Schema class has getDefaultValue():org.codehaus.jackson.JsonNode.  So Swagger interprets these and adds them to the model.  Unfortunately, those two classes are badly formed POJOs and this makes the Swagger UI blow up.

The resolution was to ignore the class path for the object, using:

ModelConverters.getInstance().addPackageToSkip("com.my.avro.model");

Unfortunately, you can only skip classes that are used from the Rest interface.  You can't skip dependent packages...  Also, there is a way to write a ModelConverter for the specific class to ignore the "schema" property, but that is just asking for bloated JSON configuration...

Ron Ratovsky

unread,
Jul 5, 2015, 3:49:20 PM7/5/15
to swagger-sw...@googlegroups.com
Hi Matt,

Thanks for the input. Feel free to open feature/improvement requests on swagger-core.

Thanks,
Ron

--
You received this message because you are subscribed to the Google Groups "Swagger" group.
To unsubscribe from this group and stop receiving emails from it, send an email to swagger-swaggers...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.
Reply all
Reply to author
Forward
0 new messages