OPENAPI/Swagger codegen AdditionnalProperties extends HashMap : play(jackson) deserialization failure.
Asked Answered
C

2

6

My problem is a little bit complicated, I'll try to explain it clearly. To do it, I've done a simple project.

I'm using Swagger codegen to generate Java classes from swagger file. In the swagger file, a definition is using additionnalProperties.

  MyRequestBody:
    type: object
    properties:
      property1:
        type: string
      property2:
        type: string
    additionalProperties:
      type: object

The generated java class :

/*
 * Chatbot api
 * Api for chatbot interface.
 *
 * OpenAPI spec version: 1.0
 * 
 *
 * NOTE: This class is auto generated by the swagger code generator program.
 * https://github.com/swagger-api/swagger-codegen.git
 * Do not edit the class manually.
 */


package lu.post.models.api.test;

import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import javax.xml.bind.annotation.*;
/**
 * MyRequestBody
 */
@javax.annotation.Generated(value = "lu.post.codegen.ApiplaylibGenerator")
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@XmlRootElement(name="MyRequestBody")
@XmlAccessorType(XmlAccessType.FIELD)
public class MyRequestBody extends HashMap<String, Object> {
  @JsonProperty("property1")
        @XmlElement(name="property1")
  private String property1 = null;

  @JsonProperty("property2")
        @XmlElement(name="property2")
  private String property2 = null;

  public MyRequestBody property1(String property1) {
    this.property1 = property1;
    return this;
  }

   /**
   * Get property1
   * @return property1
  **/
  @ApiModelProperty(example = "null", value = "")
  public String getProperty1() {
    return property1;
  }

  public void setProperty1(String property1) {
    this.property1 = property1;
  }

  public MyRequestBody property2(String property2) {
    this.property2 = property2;
    return this;
  }

   /**
   * Get property2
   * @return property2
  **/
  @ApiModelProperty(example = "null", value = "")
  public String getProperty2() {
    return property2;
  }

  public void setProperty2(String property2) {
    this.property2 = property2;
  }


  @Override
  public boolean equals(java.lang.Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || !(o instanceof MyRequestBody)) {
      return false;
    }
    MyRequestBody myRequestBody = (MyRequestBody) o;
    return Objects.equals(this.property1, myRequestBody.property1) &&
        Objects.equals(this.property2, myRequestBody.property2) &&
        super.equals(o);
  }

  @Override
  public int hashCode() {
    return Objects.hash(property1, property2, super.hashCode());
  }


  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    sb.append("class MyRequestBody {\n");
    sb.append("    ").append(toIndentedString(super.toString())).append("\n");
    sb.append("    property1: ").append(toIndentedString(property1)).append("\n");
    sb.append("    property2: ").append(toIndentedString(property2)).append("\n");
    sb.append("}");
    return sb.toString();
  }

  /**
   * Convert the given object to string with each line indented by 4 spaces
   * (except the first line).
   */
  private String toIndentedString(java.lang.Object o) {
    if (o == null) {
      return "null";
    }
    return o.toString().replace("\n", "\n    ");
  }

}

As you can see, the generated class extends HashMap for additionnalProperties.

At this stage, nothing shocking.

This class has been used in a play/java project, using play libraries to serialize/deserialize json and pojo.

I've create a simple route and controller to do a POST /test with the following body (which match with the swagger definition)

{
    "property1": "p1", 
    "property2": "p2"
}

And my controller looks like :

public Result test() {
    classLogger.debug("==================================");
    classLogger.debug("START test()");
    JsonNode bodyJsonNode = request().body().asJson();
    MyRequestBody myRequestBody = Json.fromJson(bodyJsonNode, MyRequestBody.class);

    classLogger.debug("myRequestBody : ");
    classLogger.debug(myRequestBody.toString());

    classLogger.debug("END test()");
    classLogger.debug("==================================");
    return ok();
}

And the logs show the problem :

2017-12-16 22:54:15,556[DEBUG][][][][ConversationController]==================================
2017-12-16 22:54:15,556[DEBUG][][][][ConversationController]START test()
2017-12-16 22:54:15,605[DEBUG][][][][ConversationController]myRequestBody :
2017-12-16 22:54:15,605[DEBUG][][][][ConversationController]class MyRequestBody {
    {property2=p2, property1=p1}
    property1: null
    property2: null
}
2017-12-16 22:54:15,605[DEBUG][][][][ConversationController]END test()
2017-12-16 22:54:15,605[DEBUG][][][][ConversationController]==================================

The object fields "property1" and "property2" are null, because the field name and value are put in the Map key/value.

Does anybody know the best way to resolve this problem, knowing that : - I can't modify the swagger definition (because in my real projet, it is provided by another society). - I wish to continue to use the swagger codegen library.

Thanks in advance,

Ceiba answered 16/12, 2017 at 22:16 Comment(3)
Hello, Up. Is someone encountered this problem?Ceiba
I know the question is a bit old, but I've just faced the same problem with the files produced by openApi generatorFeune
I used this with ObjectMapper: OBJECT_MAPPER.configOverride(MyClass.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.OBJECT));Feune
M
0

I'm facing another problem that seems to correlate with yours. I have managed to separate the properties and the map at my API definition, It may help you there, but the generated code is faulty because it lacks the import of HashMap.java in spite of using it.

schemas:

ReportRequest:
  type: object
  properties:
    authentication:
      $ref: '#/components/schemas/SessionAuthentication'
    reportParameters:
      $ref: '#/components/schemas/ReportParameters'
ReportParameters:
  type: object
  properties:
    fromDate:
      type: string
    toDate:
      type: string
  required:
    - fromDate
    - toDate
  additionalProperties: true
  example:  
    fromDate: '2020-04-13'
    toDate: '2022-04-13'

Notice, missing java.util.HashMap import and Typing error.

Type mismatch: cannot convert from HashMap<String,Object> to ReportParameters

    package io.swagger.model;

import java.util.Objects;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonCreator;
import io.swagger.model.ReportParameters;
import io.swagger.model.SessionAuthentication;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.*;

/**
 * ReportRequest
 */
@Validated
@javax.annotation.Generated(value = "io.swagger.codegen.v3.generators.java.SpringCodegen", date = "2022-03-31T11:12:13.135Z[GMT]")

public class ReportRequest   {
          @JsonProperty("authentication")
          private SessionAuthentication authentication = null;
        
          @JsonProperty("reportParameters")
          private ReportParameters reportParameters = new HashMap<String, Object>();

More about the assitionalProperties can be found at

Msg answered 31/3, 2022 at 11:33 Comment(0)
C
0

Not ideal but a work around is this. Define this abstract class:

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

public abstract class AbstractAdditionalPropertiesDeserializer<T extends HashMap<String, Object>> extends JsonDeserializer<T> {

    private final Class<T> type;

    public AbstractAdditionalPropertiesDeserializer(Class<T> type) {
        this.type = type;
    }

    @Override
    public T deserialize(JsonParser jsonParser, DeserializationContext ctxt)
        throws IOException {
        ObjectCodec oc = jsonParser.getCodec();
        Map<String, Object> node = oc.readValue(jsonParser, Map.class);
        try {
            T obj = type.getConstructor().newInstance();
            obj.putAll(node);
            for( Map.Entry<String, Class<?>> prop : getProperties().entrySet() ) {
                type.getDeclaredMethod("set" + capitalise(prop.getKey()),prop.getValue()).invoke(obj,node.get(prop.getKey()));
            }
            return obj;
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
            throw new IOException(e);
        }
    }

    private String capitalise(String str) {
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }

    abstract Map<String,Class<?>> getProperties();
}

Then define a bean that implements it like this:

public class MyRequestBodyDeserialiser extends AbstractAdditionalPropertiesDeserializer<MyRequestBody> {

    public MyRequestBodyDeserialiser() {
        super(MyRequestBody.class);
    }

    @Override
    Map<String,Class<?>> getProperties() {
        return new HashMap<String,Class<?>>() {
            {
                put( "property1" ,String.class);
            }
        };
    }
}

Will inject it into your object mapper configuration and allow you do deserialize a JSON payload that will additionally set the required properties

Cedilla answered 3/3, 2023 at 12:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.