swagger-api / swagger-core

Examples and server integrations for generating the Swagger API Specification, which enables easy access to your REST API
http://swagger.io
Apache License 2.0
7.38k stars 2.18k forks source link

Mongo ObjectId mapping issue in swagger mapper #720

Closed mukulgupta2507 closed 10 years ago

mukulgupta2507 commented 10 years ago

Hi, Swagger is not able to map Mongo ObjectId correctly. My model class looks something like this:

public class Person implements Serializable { @org.jongo.marshall.jackson.oid.Id @ObjectId private String id; // internal mongo db id ...

Swagger gives me response something like this:

  "_id": {
    "date": 1343026573000,
    "time": 1343026573000,
    "timestamp": 1343026573,
    "new": false,
    "timeSecond": 1343026573,
    "inc": 402653369,
    "machine": 808430687
  }

Instead I expect something like this: "_id": "406b5b3de4b8dde5f3000000"

I have seen already existing issues and tried following things but nothing seems to work.

  1. Using @ApiModelProperty(dataType= "string") annotation of my id field but it's not working.
  2. I tried using Overriding model in my bootstrap servlet but no help.

import com.wordnik.swagger.converter.*;

String jsonString = "{" + " \"_id\" : \"ObjectId\" "+ "}"; OverrideConverter converter = new OverrideConverter(); converter.add(ObjectId.class.getName(), jsonString); ModelConverters.addConverter(converter, true);

But it's not working, may be I'm doing it wrong way.

  1. I also tried using Typeconverter but didn't solve the issue:

    TypeConverter typeConverter = new TypeConverter(); typeConverter.add("_id", "string"); ModelConverters.addConverter(typeConverter, true);

Anyone who has faced the same issue and able to find any work around for this, please revert.

Thanks

webron commented 10 years ago

Which version of swagger-core do you use? Which dependency do you use specifically?

mukulgupta2507 commented 10 years ago

@webron : I'm using swagger core 1.3.10 http://mvnrepository.com/artifact/com.wordnik/swagger-core_2.10/1.3.10. In addition to this, I'm also using swagger-jaxrs_2.10 and swagger-annotations. http://mvnrepository.com/artifact/com.wordnik/swagger-annotations

These are all libraries that I'm using for swagger related stuff. In addition to this for interacting with db, I 'm using jongo annotations which I have shown in the above mentioned class.

webron commented 10 years ago

Can you try putting the @ApiModelProperty on the getter of the field rather than on the field itself? I tested it not too long ago and it worked just fine.

mukulgupta2507 commented 10 years ago

I tried using @ApiModelProperty(dataType= "string") on the getter of the field but still it doesn't work. BTW I'm also using @XmlElement(name = "id") on the getter field since my rest api supports both json and xml. Can this be one problem ? Or the way I'm using @ApiModelProperty is not correct ?

webron commented 10 years ago

It shouldn't be a problem but for testing purposes you can try removing the @XmlElement and see if it works.

mukulgupta2507 commented 10 years ago

No, I tried it after removing @XmlElement but still doesn't work. Any other work around ?

webron commented 10 years ago

How do you use that model? As a response or parameter? Can you share the relevant method signature? On Oct 10, 2014 4:45 PM, "Mukul Gupta" notifications@github.com wrote:

No, I tried it after removing @XmlElement but still doesn't work. Any other work around ?

— Reply to this email directly or view it on GitHub https://github.com/wordnik/swagger-core/issues/720#issuecomment-58657250 .

mukulgupta2507 commented 10 years ago

I'm using that model as a reponse of my rest api. which method signature should I share with you ?

webron commented 10 years ago

The one using it as a response, including the annotations. On Oct 10, 2014 4:52 PM, "Mukul Gupta" notifications@github.com wrote:

I'm using that model as a reponse of my rest api. which method signature should I share with you ?

— Reply to this email directly or view it on GitHub https://github.com/wordnik/swagger-core/issues/720#issuecomment-58658217 .

mukulgupta2507 commented 10 years ago

One more thing I'm not using @ApiModel on the main POJO class. Is it necessary to first use annotation @ApiModel on POJO ?

Method signature of my rest api is:

@ApiOperation(value="Find Id",response=MyPojo.class) public javax.ws.rs.core.Response getMethod(@ApiParam(value="ID of object",required=true) @PathParam("id") final String id, @ApiParam(value="type",required=false,allowMultiple=true) @QueryParam("type") final String type, @ApiParam(value="keys",required=false,allowMultiple=true) @QueryParam("keys") final String keys)

webron commented 10 years ago

And I assume MyPojo.class is the one containing that field?

mukulgupta2507 commented 10 years ago

Yeah, that's right.

webron commented 10 years ago

There's something that doesn't quite add up here. In the original post you have a code sample like this:

public class Person implements Serializable {
   @org.jongo.marshall.jackson.oid.Id
   @ObjectId
   private String id; // internal mongo db id
   ...

That field is actually a String a not Mongo's ObjectId, which indeed translates to what you posted above when using its .toString(). So, are you trying to convert that field or another one? It could be useful to see the whole actual pojo if you can share it.

mukulgupta2507 commented 10 years ago

I'll try to clear your doubts. I have represented my mongo id as a string in my Java POJO, now since jongo mapper which I normally use to query my mongo db automatically converts into an ObjectId, I need not to worry about it. Using @ObjectId ensures that. But that thing is not happening when I started using swagger. I know the issue lies with the type of object mapper which internally swagger uses to process objectId. I'm trying to handle this field only but without breaking it into time,new,timestamp,machine,etc. It's happening something like a Date Object, when you know that swagger actually breaks into smaller things like minutes,seconds,hours,etc. Is there anyway I can tell swagger that don't process it this way. Simply handle it as an ObjectId ?

I have mostly mosted the main parts of POJO. Othere parts of POJO contains other fields, I don't think there is anything you can find from there.

fehguy commented 10 years ago

I suggest making a test case in swagger-core so we can address this.

mukulgupta2507 commented 10 years ago

@webron @fehguy Is there anyway by which I can use Swagger Overriding models functionality to override ObjectId models. There is a blog post which shows how to override Date models. Can we do something similar to this ?

https://github.com/wordnik/swagger-core/wiki/overriding-models

webron commented 10 years ago

You can, I just don't think it's needed. If you can provide us with a test case, we could investigate it further as to why it doesn't natively work.

mukulgupta2507 commented 10 years ago

Okay, for test cases should I write a sample pojo with all the necessary annotations and a sample rest api which will be using it and post it here with the corresponding output and expected outputs ?

Also, if you can give me some hints on the overriding models parts of swagger for ObjectId, it'll be great. I tried writing models something like:

        String jsonString = "{" +
        "  \"_id\": \"ObjectId\"," +
        "  \"properties\": {" +
        "      \"date\": \"Date in ISO-8601 format\"," +
        "      \"time\": \"Date in ISO-8601 format\"," +
        "      \"timestamp\": \"Date in ISO-8601 format\"," +
        "      \"new\": false," +
        "      \"timeSecond\": \"Date in ISO-8601 format\"," +
        "      \"inc\": \"Date in ISO-8601 format\"," +
        "      \"machine\": \"Date in ISO-8601 format\"" +
        "    }" +
        "  }" +
        "}";
        OverrideConverter converter = new OverrideConverter();
        converter.add(ObjectId.class.getName(), jsonString);
        ModelConverters.addConverter(converter, true);

But it didn't work.

webron commented 10 years ago

Yeah, the test case should be something I could run locally without too much work (I'll give you a hint, I don't have mongodb installed ;) - though that shouldn't matter for the test case)).

As for the converter, in which part of your code did you register it?

mukulgupta2507 commented 10 years ago

Okay, for the test cases I'll write a simple POJO with all the necessary annotations and a simple REST api so that you can test it on your local machine.

As for the converter, I have written a bootstrap servlet file which is initialized when Resteasy scans all my resources.

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;

import org.bson.types.ObjectId;

import com.wordnik.swagger.config.ConfigFactory;
import com.wordnik.swagger.config.ScannerFactory;
import com.wordnik.swagger.config.SwaggerConfig;
import com.wordnik.swagger.converter.ModelConverters;
import com.wordnik.swagger.converter.OverrideConverter;
import com.wordnik.swagger.converter.TypeConverter;
import com.wordnik.swagger.jaxrs.config.ReflectiveJaxrsScanner;
import com.wordnik.swagger.jaxrs.reader.DefaultJaxrsApiReader;
import com.wordnik.swagger.reader.ClassReaders;

public class Bootstrap extends HttpServlet {
    @Override
    public void init(ServletConfig servletConfig) {
        try {
            super.init(servletConfig);
            SwaggerConfig swaggerConfig = new SwaggerConfig();
            ConfigFactory.setConfig(swaggerConfig);
            swaggerConfig.setBasePath("http://localhost:8080/myresource");
            swaggerConfig.setApiVersion("1.0.0");
            ReflectiveJaxrsScanner scanner = new ReflectiveJaxrsScanner();
            scanner.setResourcePackage("my_package"); //your "resources" package
            ScannerFactory.setScanner(scanner);
            ClassReaders.setReader(new DefaultJaxrsApiReader());
            String jsonString = "{" +
            "  \"_id\": \"ObjectId\"," +
            "  \"properties\": {" +
            "      \"date\": \"Date in ISO-8601 format\"," +
            "      \"time\": \"Date in ISO-8601 format\"," +
            "      \"timestamp\": \"Date in ISO-8601 format\"," +
            "      \"new\": false," +
            "      \"timeSecond\": \"Date in ISO-8601 format\"," +
            "      \"inc\": \"Date in ISO-8601 format\"," +
            "      \"machine\": \"Date in ISO-8601 format\"" +
            "    }" +
            "}";
            OverrideConverter converter = new OverrideConverter();
            converter.add(ObjectId.class.getName(), jsonString);
            ModelConverters.addConverter(converter, true);
        } catch (ServletException e) {
            System.out.println(e.getMessage());
        }
    }
}

I have copied my entire bootstrap file here. I guess this is how we override the way swagger looks the things. But it's not working, Any ideas ?

webron commented 10 years ago

Have you registered the Bootstrap in your web.xml?

mukulgupta2507 commented 10 years ago

Yeah. I have done it like this:

<servlet>
    <servlet-name>Bootstrap</servlet-name>
    <servlet-class>my_package.Bootstrap</servlet-class>
    <load-on-startup>2</load-on-startup>
 </servlet> 

BootStrap is working fine, that's for sure because that way only I made swagger to scan my resources using DefaultJaxrsApiReader.

webron commented 10 years ago

It looks right, and unfortunately I can't test it at the moment. The only thing I'd suggest trying is registering the converter before initializing the scanner and reader (no idea if that would help).

mukulgupta2507 commented 10 years ago

No, It's not working. Infact I'm getting an exception like this:

2014-10-20 15:32:05,246: ERROR com.wordnik.swagger.converter.OverrideConverter - failed to convert json to model org.json4s.package$MappingException: Did not find value which can be converted into java.lang.String at org.json4s.reflect.package$.fail(package.scala:96) at org.json4s.Extraction$.convert(Extraction.scala:554) at org.json4s.Extraction$.extract(Extraction.scala:331) at org.json4s.Extraction$.extract(Extraction.scala:42) at org.json4s.ExtractableJsonAstNode.extract(ExtractableJsonAstNode.scala:21) at com.wordnik.swagger.model.SwaggerSerializers$JsonSchemaModelSerializer$$anonfun$$init$$1$$anonfun$apply$1$$anonfun$applyOrElse$9.apply(SwaggerSerializers.scala:51) at com.wordnik.swagger.model.SwaggerSerializers$JsonSchemaModelSerializer$$anonfun$$init$$1$$anonfun$apply$1$$anonfun$applyOrElse$9.apply(SwaggerSerializers.scala:51) at org.json4s.ExtractableJsonAstNode.extractOrElse(ExtractableJsonAstNode.scala:59) at com.wordnik.swagger.model.SwaggerSerializers$JsonSchemaModelSerializer$$anonfun$$init$$1$$anonfun$apply$1.applyOrElse(SwaggerSerializers.scala:51) at com.wordnik.swagger.model.SwaggerSerializers$JsonSchemaModelSerializer$$anonfun$$init$$1$$anonfun$apply$1.applyOrElse(SwaggerSerializers.scala:33) at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:33) at org.json4s.CustomSerializer$$anonfun$deserialize$1.applyOrElse(Formats.scala:364) at org.json4s.CustomSerializer$$anonfun$deserialize$1.applyOrElse(Formats.scala:362) at scala.runtime.AbstractPartialFunction.apply(AbstractPartialFunction.scala:33) at org.json4s.CustomSerializer$$anonfun$deserialize$1.applyOrElse(Formats.scala:362) at org.json4s.CustomSerializer$$anonfun$deserialize$1.applyOrElse(Formats.scala:362) at scala.PartialFunction$OrElse.apply(PartialFunction.scala:162) at org.json4s.CustomSerializer$$anonfun$deserialize$1.applyOrElse(Formats.scala:362) at org.json4s.CustomSerializer$$anonfun$deserialize$1.applyOrElse(Formats.scala:362) at scala.PartialFunction$OrElse.apply(PartialFunction.scala:162)

I'll post the test cases today when I'm free. Meanwhile, if you get a chance, can you please see what's wrong with the model part ?

webron commented 10 years ago

Okay, that error actually implies that the converter is used, so that's a step in the right direction.

I believe the problem is with the jsonString you use, but now that I think about it, the converter is probably not the right way to go. The converter can be used to replace a one model representation with another model representation, but in your case, you want to replace a model with a primitive.

Can you share your whole model definition as-is?

mukulgupta2507 commented 10 years ago

I want to convert following model to MongoDB ObjectId without breaking the original id into sub-fields: "_id": { "date": 1337063806000, "time": 1337063806000, "timestamp": 1337063806, "new": false, "timeSecond": 1337063806, "inc": -100663295, "machine": 808430657 } Rather I want it to be coming like this:

             "_id" : "503b2ab3e4b032e338f11111"

If you want something else, then please let me know.

webron commented 10 years ago

Yeah, I'm familiar with MongoDB's ObjectIds (just haven't used it for a while now).

I'd like to see a full model class containing such an ObjectID whose conversion gives you the unwanted results, assuming you can share it. If not, then we'll have to wait for the test case (which may still be needed even if you provide the full model class).

mukulgupta2507 commented 10 years ago

@webron @fehguy Okay, so I'm posting a simple test case and the process to reproduce it. I have created a test collection in my MongoDB in test DB on localhost and inserted one document:

    > use test
    > db.test.insert({name:"somerandomname"});
    > db.test.findOne()
    { "_id" : ObjectId("544507e70ffab1fab3a6c492"), "name" : "somerandomname" }

My POJO looks like this:

import java.io.Serializable;
import org.jongo.marshall.jackson.oid.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Field;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class SampleModel implements Serializable{

    private static final long serialVersionUID = -7917366681345859073L;

    @Id
    @org.jongo.marshall.jackson.oid.Id
    @ObjectId
    private String id;
    @Field("name")
    @JsonProperty("name")
    private String name;

    public SampleModel() {
    }

    //@ApiModelProperty(dataType= "string")  I tried using this but it's not working  
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }

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

}

And My Rest API looks like this:

@Path("/v1/samplerestapi")
@Component
@Api(value="/v1/samplerestapi",description="operations about sample rest api")
@Produces({MediaType.APPLICATION_JSON})
public class SampleRestAPI{
    @GET
    @Path("/name/{name}")
    @Produces({MediaType.APPLICATION_JSON})
    @ApiOperation(value="find document by name",response=SampleModel.class)
    @ApiResponses(value={
            @ApiResponse(code=200,message="Success"),
            @ApiResponse(code=500,message="Exception occurred"),
            @ApiResponse(code=404,message="Not Found")
    })
    public javax.ws.rs.core.Response getDocumentByName(@ApiParam(value="name of document to fetch",required=true) @PathParam("name") final String name) {
        try{
            if(name != null){
                MongoCollection jongo = new Jongo(new MongoClient("localhost").getDB("test")).getCollection("test");
                SampleModel obj = jongo.findOne(new BasicDBObject().append("name",name).toString()).as(SampleModel.class);
                return javax.ws.rs.core.Response.status(Status.BAD_REQUEST).entity(obj).build();
            }
            else{
                return javax.ws.rs.core.Response.status(Status.BAD_REQUEST).entity("Not found").build();
            }
        }catch(Exception e){
            return javax.ws.rs.core.Response.status(Status.INTERNAL_SERVER_ERROR).entity("Not found").build();
        }
    }
}

So the hit which you will be using is: http://localhost:8080/sampleproject/api/v1/samplerestapi/name/somerandomname

And the output will be:

{
    "name": "somerandomname",
    "_id": {
        "date": 1413810151000,
        "time": 1413810151000,
        "timestamp": 1413810151,
        "new": false,
        "timeSecond": 1413810151,
        "inc": -1280916334,
        "machine": 268087802
    }
}

But I expect something like this:

{ "_id" : "544507e70ffab1fab3a6c492", "name" : "somerandomname" }

I guess that will work. Rest of the things you can configure on your local environment like adding swagger dependency and RestEasy configuration in your pom.xml and web.xml.

Please revert if anything is still not clear.

webron commented 10 years ago

But if I understand correctly, what you're describing here is operating your own API, not looking at the generated Swagger specification. Swagger does not affect your API in any way.

mukulgupta2507 commented 10 years ago

But in my case it's affecting the API. If the remove swagger annotations then the output will come as expected. Adding the swagger annotations is the only change I have made in my Rest API's.

fehguy commented 10 years ago

Hi, I think that's not possible. The swagger annotations don't affect any of the runtime of the API, just the description of it.

webron commented 10 years ago

Do you have any custom Jackson configuration in your application?

mukulgupta2507 commented 10 years ago

No, I'm just using Jongo Annotations which internally uses Jackson itself. Also, since I'm working on legacy code so Spring Annotations are also there. I guess those are not creating any problem. There is something wrong in the mapper only but not able to figure out why it's happening only when using Swagger Annotations in my code. Is there something like when you install swagger then it overrides the default Jackson mapper because that's the only possible case if swagger doesn't change anything in the API.

webron commented 10 years ago

https://github.com/bguerout/jongo/issues/154 shows an example of someone using both Jongo and Swagger, and they haven't reported an issue such as the one you describe (though granted, they use a different version of Swagger).

To be clear, which Swagger annotations you remove for it to work properly?

We can also investigate further. Any chance you can share the output of mvn dependency:tree?

King-Wizard commented 10 years ago

Hello guys,

I read all your discussion and actually the bug comes from an update of the Jongo library 1.0 to 1.1.

For a solution to your problem, please find an answer here: http://stackoverflow.com/questions/26474514/using-jongo-and-jackson-2-how-to-deserialize-a-mongodb-objectid-represented-un

I encountered the same problem today with jackson 2, jersey 2 and Jongo 1.1.

Note: do not forget to put +1 on my answer on stackoverflow.

Cheers guys.

From France.

mukulgupta2507 commented 10 years ago

@alino91 you are right, it's the issue of Jongo not Swagger. @webron The solution suggested by @alino91 is working for me right now. I guess I can use it till we get a fix in the Jongo itself.

For those who are facing the same problem and looking for a solution, you can follow the @alino91 answer, it will work fine.

Thanks for the help guys.

Cheers

webron commented 10 years ago

@alino91 - thank you for sharing this information!

@mukulgupta2507 - glad you have it sorted out for now. Closing the issue now, please reopen if needed.

King-Wizard commented 10 years ago

Another solution to solve this problem would consist in using Jongo.

See the following last post for a detailed explanation: https://github.com/bguerout/jongo/issues/221

razonrus commented 5 years ago

Try to use OperationFilter:

public class SwaggerOperationFilter : IOperationFilter
    {
        private readonly IEnumerable<string> objectIdIgnoreParameters = new[]
        {
            nameof(ObjectId.Timestamp),
            nameof(ObjectId.Machine),
            nameof(ObjectId.Pid),
            nameof(ObjectId.Increment),
            nameof(ObjectId.CreationTime)
        };

        public void Apply(Operation operation, OperationFilterContext context)
        {
            operation.Parameters = operation.Parameters.Where(x =>
                x.In != "query" || objectIdIgnoreParameters.Contains(x.Name) == false
            ).ToList();
        }
    }

and

options.OperationFilter<SwaggerOperationFilter>();