Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Java] Swagger oneOf type: Jackson trying to instantiate interface instead of implementation? #10011

Closed
rorytorneymf opened this issue Jan 29, 2020 · 24 comments
Assignees

Comments

@rorytorneymf
Copy link

rorytorneymf commented Jan 29, 2020

Description

I'm using the oneOf feature to define several possible schemas that can go into a request body property of my service. In the generated Java client code, the Java implementations of these schemas implement an interface, but when I send a request through, Jackson is trying to create an instance of the interface, instead of the concrete class.

Swagger-codegen version
<groupId>io.swagger.codegen.v3</groupId>
<artifactId>swagger-codegen-maven-plugin</artifactId>
<version>3.0.14</version>
Swagger declaration file content or url
schemas:
    TestRequest:
      description: 
        Test request
      type:
        object
      required:
        - criteria
      properties:
        criteria:
          oneOf:
           - $ref: '#/components/schemas/CriteriaA'
           - $ref: '#/components/schemas/CriteriaB'
          discriminator:
            propertyName: type
            mapping:
              CriteriaA: '#/components/schemas/CriteriaA'
    ...
    CriteriaA:
      description: Criteria A
      type: object
      required:
        - type
        - query
      properties:
        type: 
          description: A description
          type: string
          enum:
           - CriteriaA
      query:
        description: A query.
        type: object
Command line used for generation

Maven plugin specified above used for generation.

Steps to reproduce

The Java client code generated by swagger codegen looks like this:

Interface:

public interface OneOfTestRequestCriteria {}

Concrete class:

@Schema(description = "")
@javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.JavaClientCodegen", date = "2020-01-28T13:06:29.942Z[Europe/London]")
public class CriteriaA implements OneOfTestRequestCriteria {

  @JsonAdapter(TypeEnum.Adapter.class)
  public enum TypeEnum {
    CriteriaA("CriteriaA");

    private String value;

    TypeEnum(String value) {
      this.value = value;
    }
    public String getValue() {
      return value;
    }

    @Override
    public String toString() {
      return String.valueOf(value);
    }
    public static TypeEnum fromValue(String text) {
      for (TypeEnum b : TypeEnum.values()) {
        if (String.valueOf(b.value).equals(text)) {
          return b;
        }
      }
      return null;
    }
    public static class Adapter extends TypeAdapter<TypeEnum> {
      @Override
      public void write(final JsonWriter jsonWriter, final TypeEnum enumeration) throws IOException {
        jsonWriter.value(enumeration.getValue());
      }

      @Override
      public TypeEnum read(final JsonReader jsonReader) throws IOException {
        String value = jsonReader.nextString();
        return TypeEnum.fromValue(String.valueOf(value));
      }
    }
  }  @SerializedName("type")
  private TypeEnum type = null;

  @SerializedName("query")
  private Object query = null;

  public CriteriaA type(TypeEnum type) {
    this.type = type;
    return this;
  }

  @Schema(required = true, description = "")
  public TypeEnum getType() {
    return type;
  }

  public void setType(TypeEnum type) {
    this.type = type;
  }

  public CriteriaA query(Object query) {
    this.query = query;
    return this;
  }

  @Schema(required = true, description = "")
  public Object getQuery() {
    return query;
  }

  public void setQuery(Object query) {
    this.query = query;
  }


  @Override
  public boolean equals(java.lang.Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }
    CriteriaA criteriaA = (CriteriaA ) o;
    return Objects.equals(this.type, criteriaA.type) &&
        Objects.equals(this.query, criteriaA.query);
  }

  @Override
  public int hashCode() {
    return Objects.hash(type, query);
  }


  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("class CriteriaA {\n");

    sb.append("    type: ").append(toIndentedString(type)).append("\n");
    sb.append("    query: ").append(toIndentedString(query)).append("\n");
    sb.append("}");
    return sb.toString();
  }

  private String toIndentedString(java.lang.Object o) {
    if (o == null) {
      return "null";
    }
    return o.toString().replace("\n", "\n    ");
  }

}

I'm trying to use this generated client code to send a request:

final TestRequest testRequest = new TestRequest();

final CriteriaA criteriaA = new CriteriaA ();
criteriaA .setType(CriteriaA .TypeEnum.CriteriaA);
criteriaA .setQuery("a query");

testRequest .setCriteria(criteriaA );

final ApiResponse<Void> apiResponse = testApi.createOrUpdateTestWithHttpInfo(testRequest);

Running the above client code results in this error when Jackson tries to deserialize it. It seems to be trying to construct an instance of the interface OneOfTestRequestCriteria, instead of the concrete implementation of the interface; CriteriaA:

[Request processing failed; nested exception is org.springframework.http.converter.HttpMessageConversionException: Type definition error: [simple type, class com.acme.tag.models.OneOfTestRequestCriteria]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of com.acme.tag.models.OneOfTestRequestCriteria (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information\n

Related issues/PRs

OpenAPITools/openapi-generator#4785

@ghost
Copy link

ghost commented Jan 29, 2020

I've run into a very similar issue, if i use anyOf or oneOf and generate a java client I run into issues where it has created an interface called AnyOfBody and then a basic implementation of it called Body. All of my generated classes are implementations of AnyOfBody but the api client methods are all expecting the input of type Body so all my request objects do not match. Because of this i am unable to execute any of the api calls that use oneOf or anyOf in the OAS.

So far i haven't found a solution

@rorytorneymf
Copy link
Author

If I annotate the generated interface:

public interface OneOfTestRequestCriteria {}

with the following:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME, 
  include = JsonTypeInfo.As.PROPERTY, 
  property = "type")
@JsonSubTypes({ 
  @Type(value = CriteriaA.class, name = "CriteriaA")
})
public interface OneOfTestRequestCriteria {

}

Then the request gets deserialized correctly into CriteriaA - am I missing something in my swagger.yaml that would result in this interface not getting annotated by the codegen tool?

@dinesh361
Copy link

@rorytorneymf I have the same issue. Were you able to identify the issue?

@HugoMario HugoMario self-assigned this Mar 28, 2020
@frantuma frantuma assigned gracekarina and unassigned HugoMario Apr 22, 2020
@DenisIstomin
Copy link

I have the same issue.
@rorytorneymf, thanks, adding annotations helps.
Any plans for implementing this? @gracekarina @frantuma

@implacablucien
Copy link

Same problem here. I can make do with a custom deserializer but it would be nice if generation took care of it ^^

@vnosach-clgx
Copy link

I had the same problem until I configured MixIn. I mapped the generated interface to implemented superclass with existing @JsonSubTypes and @JsonTypeInfo.

It seems like objectMapper.addMixIn(OneOfTestRequestCriteria.class, CriteriaA.class)

@swrnm
Copy link

swrnm commented Sep 10, 2020

I've run into a very similar issue, if i use anyOf or oneOf and generate a java client I run into issues where it has created an interface called AnyOfBody and then a basic implementation of it called Body. All of my generated classes are implementations of AnyOfBody but the api client methods are all expecting the input of type Body so all my request objects do not match. Because of this i am unable to execute any of the api calls that use oneOf or anyOf in the OAS.

So far i haven't found a solution

Facing exact same issue
Has anyone found the solution ?

@sbilello
Copy link

+1 Same Issue

@mikekonan
Copy link

any news?

@sauravInfoObject
Copy link

any update on this

@mariangeorgescu
Copy link

+1 Same Issue

@levischuckeats
Copy link

Hi, this issue hit our team too.

@msmerc
Copy link

msmerc commented Oct 4, 2021

There seem to be a bunch of issues with AnyOf / AllOf / OneOf - for example: swagger-api/swagger-codegen-generators#966

Is there any plan to address them / would you accept community submissions / bounties for addressing these? [@HugoMario ?]

@ghost
Copy link

ghost commented Oct 7, 2021

I've hit this as well. It's not usable.
In xml it is an easy thing for a generator to deal with, xjc from xmlschema for an oneOf (choice in xml) would just put the two classes in, and one would be null, the other populated. This works because xml follows order.

So I realize json that's not going to work, these generators need to do tricky jackson stuff.

My workaround (if I stick with openapi at all) is to change oneOf to just a named references, and only use one of them. I think that would work well enough.

In using openapi, coming from xmlschema, either I'm missing a lot since I'm new, or the tools are still pretty green in some areas.

@ahnaidu
Copy link

ahnaidu commented Nov 7, 2021

If anyone got the solution. Please publish it.

@ahnaidu
Copy link

ahnaidu commented Nov 7, 2021

If I annotate the generated interface:

public interface OneOfTestRequestCriteria {}

with the following:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME, 
  include = JsonTypeInfo.As.PROPERTY, 
  property = "type")
@JsonSubTypes({ 
  @Type(value = CriteriaA.class, name = "CriteriaA")
})
public interface OneOfTestRequestCriteria {

}

Then the request gets deserialized correctly into CriteriaA - am I missing something in my swagger.yaml that would result in this interface not getting annotated by the codegen tool?

I am also facing the same issue. What is the way to do through yml file.

@HugoMario
Copy link
Contributor

sorry for delay response, i'm going to implement the proposed solution.

@HugoMario HugoMario self-assigned this Dec 17, 2021
@msmerc
Copy link

msmerc commented Dec 17, 2021

@HugoMario I've noticed there's a lot of issues in this space. It's especially tricky if you're trying to follow some 3rd party API that uses these heavily. If it would help I can provide some examples of real-world edge cases?

@HugoMario
Copy link
Contributor

@msmerc that would be helpful, sure. Please share the examples.

@HugoMario
Copy link
Contributor

hey guys, i added a PR to accomplish this issue

Can you please check it out with this snapshot version of swagger cli and let me know if there is something else to add?

@msmerc
Copy link

msmerc commented Dec 20, 2021

@HugoMario I've put some comments on the MR. Unfortunately it wouldn't work for my use case since the component I'm talking to is very strict about the JSON it receives and will reject this because it has an unknown field. It actually makes it worse since currently I'm able to use jackson mixins to fix the broken inheritence.

@msmerc
Copy link

msmerc commented Dec 20, 2021

@HugoMario I've created a gist with the subset of an actual Bloomberg API I'm using. See: https://gist.github.com/msmerc/bb7d4b848585a26b7baa2bd5722d1fcc
There's some heavy use of oneOf / allOf.

In particular it's worth noting they have a field that's called "@type" (with an @ in the actual json fieldname), which is what one should use to figure out what subtype to use. I appreciate that hinting to swagger that this field should be used for JSON type info is not simple!

Slightly related to this issue; another thing they are keen on is this construct:

            "@type":
              allOf:
                - $ref: "#/components/schemas/Type"
                - enum:
                    - "Dataset"

Here they are saying the type field is a string (basically what the ref is doing), but also a single-valued enum. It's a very fancy way of saying the field is always set to "Dataset"! Would be great if swagger could spot a single valued enum and handle that!

@HugoMario
Copy link
Contributor

@msmerc i'll file a new ticket to follow up what you're reporting.
This one is already addressed, and changes can be used on next release.

@ivadoncheva
Copy link

Hi @HugoMario, part of which release is this fix?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests