Search code examples
javamockmvcspring-restdocs

Restdoc throws org.springframework.restdocs.snippet.SnippetException when trying to document child objects


I have an OrderInfo class:

@ApiModel(description = "object needed to make an order")

public class OrderInfo  implements Serializable {
  private static final long serialVersionUID = 1L;

  @JsonProperty("purchase")
  private PurchaseInfo purchase = null;

  @JsonProperty("paymentMode")
  private PaymentMode paymentMode = null;

  @JsonProperty("serialNumber")
  private String serialNumber = null;

  public OrderInfo purchase(PurchaseInfo purchase) {
    this.purchase = purchase;
    return this;
  }

  @ApiModelProperty(required = true, value = "coming from commercialization")
  @NotNull
  @Valid
  public PurchaseInfo getPurchase() {
    return purchase;
  }

  public void setPurchase(PurchaseInfo purchase) {
    this.purchase = purchase;
  }

  public OrderInfo paymentMode(PaymentMode paymentMode) {
    this.paymentMode = paymentMode;
    return this;
  }

  @ApiModelProperty(required = true, value = "")
  @NotNull
  @Valid
  public PaymentMode getPaymentMode() {
    return paymentMode;
  }

  public void setPaymentMode(PaymentMode paymentMode) {
    this.paymentMode = paymentMode;
  }

  public OrderInfo serialNumber(String serialNumber) {
    this.serialNumber = serialNumber;
    return this;
  }


  @ApiModelProperty(value = "The serial number of the registered device")
  public String getSerialNumber() {
    return serialNumber;
  }

  public void setSerialNumber(String serialNumber) {
    this.serialNumber = serialNumber;
  }
}

Having a PurchaseInfo child object:

@ApiModel(description = "Info necessary to order a video, an episode or a season")

public class PurchaseInfo  implements Serializable {
  private static final long serialVersionUID = 1L;

  @JsonProperty("id")
  private String id = null;

  @JsonProperty("pricingId")
  private String pricingId = null;

  @JsonProperty("price")
  private Double price = null;

  @JsonProperty("type")
  private ArticleType type = null;

  public PurchaseInfo id(String id) {
    this.id = id;
    return this;
  }


  @ApiModelProperty(required = true, value = "id of the commercialization")
  @NotNull
  public String getId() {
    return id;
  }

  public void setId(String id) {
    this.id = id;
  }

  public PurchaseInfo pricingId(String pricingId) {
    this.pricingId = pricingId;
    return this;
  }

  @ApiModelProperty(required = true, value = "additional pricing of the commercialization")
  @NotNull
  public String getPricingId() {
    return pricingId;
  }

  public void setPricingId(String pricingId) {
    this.pricingId = pricingId;
  }

  public PurchaseInfo price(Double price) {
    this.price = price;
    return this;
  }

  @ApiModelProperty(required = true, value = "price of the commercialization")
  @NotNull
  public Double getPrice() {
    return price;
  }

  public void setPrice(Double price) {
    this.price = price;
  }

  public PurchaseInfo type(ArticleType type) {
    this.type = type;
    return this;
  }

  @ApiModelProperty(required = true, value = "")
  @NotNull
  @Valid
  public ArticleType getType() {
    return type;
  }

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

And the generated corresponding FieldDescriptor classes:

public class OrderInfoFieldDescriptor {
    public static FieldDescriptor[] fdOrderInfo = new FieldDescriptor[] {
        fieldWithPath("purchase").description("coming from commercialization").type(PurchaseInfo.class),
        fieldWithPath("paymentMode").description("").type(PaymentMode.class),
        fieldWithPath("serialNumber").description("The serial number of the registered device").type(java.lang.String.class).optional()    };


    public static FieldDescriptor[] fdOrderInfoList = new FieldDescriptor[] {
        fieldWithPath("[].purchase").description("coming from commercialization").type(PurchaseInfo.class),
        fieldWithPath("[].paymentMode").description("").type(PaymentMode.class),
        fieldWithPath("[].serialNumber").description("The serial number of the registered device").type(java.lang.String.class).optional()    };
}

And:

public class PurchaseInfoFieldDescriptor {
    public static FieldDescriptor[] fdPurchaseInfo = new FieldDescriptor[] {
        fieldWithPath("id").description("id of the commercialization").type(java.lang.String.class),
        fieldWithPath("pricingId").description("additional pricing of the commercialization").type(java.lang.String.class),
        fieldWithPath("price").description("price of the commercialization").type(java.lang.Double.class),
        fieldWithPath("type").description("").type(ArticleType.class)    };


    public static FieldDescriptor[] fdPurchaseInfoList = new FieldDescriptor[] {
        fieldWithPath("[].id").description("id of the commercialization").type(java.lang.String.class),
        fieldWithPath("[].pricingId").description("additional pricing of the commercialization").type(java.lang.String.class),
        fieldWithPath("[].price").description("price of the commercialization").type(java.lang.Double.class),
        fieldWithPath("[].type").description("").type(ArticleType.class)    };
}

When mockMvc.perform is called in order to make a HTTP POST with requestBody and responseBody of type OrderInfo.class:

mockMvc.perform(postRequest)
                .andDo(document("orderInfoCreate", preprocessRequest(prettyPrint()), preprocessResponse(prettyPrint()),
                        pathParameters(parameterWithName("username").description("the name of the user")),
                        responseFields(OrderInfoFieldDescriptor.fdOrderInfo),
                        requestFields(OrderInfoFieldDescriptor.fdOrderInfo))).andExpect(status().isCreated())
                .andExpect(content().json(orderInfoAsJson));

The validation works for paymentMode and serialNumber but fails for purchase with the following exception:

org.springframework.restdocs.snippet.SnippetException: The following parts of the payload were not documented:
{
  "purchase" : {
    "id" : "purchaseId",
    "pricingId" : "pricingId",
    "price" : 12.0,
    "type" : "EPISODE"
  },
  "serialNumber" : "serialNumber"
}

Even though the request body and the response body look fine:

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /api/users/myUserName/orders
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Host:"host:posrt", Accept:"application/json;charset=UTF-8", Cookie:"identity=cookieForTest"]
             Body = {"purchase":{"id":"purchaseId","pricingId":"pricingId","price":12.0,"type":"EPISODE"},"paymentMode":"POSTPAID","serialNumber":"serialNumber"}
    Session Attrs = {}

And:

MockHttpServletResponse:
           Status = 201
    Error message = null
          Headers = [Content-Type:"application/json;charset=UTF-8"]
     Content type = application/json;charset=UTF-8
             Body = {"purchase":{"id":"purchaseId","pricingId":"pricingId","price":12.0,"type":"EPISODE"},"paymentMode":"POSTPAID","serialNumber":"serialNumber"}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

The issue appeared after migrating from Spring Boot 1x to 2x.

Do you have any idea what the issue could be?

Thanks :)


Solution

  • It looks like you are relying on purchase to document both purchase and everything it contains, i.e. purchase.id, purchase.pricingId, purchase.price, and purchase.type. This worked in REST Docs up to and including 1.1 but it created a problem where people would accidentally miss documenting a nested field as they had documented its parent. This was discussed in this question and also in this Spring REST Docs issue. The issue introduced a change in REST Docs 1.2 where documenting a field no longer documents all of its descendants by default. You are affected by this change as upgrading to Spring Boot 2 means that you have also upgraded to REST Docs 2.

    If you want to keep the existing behaviour and just use purchase to document both it and all of its descendants, you should replace fieldWithPath with subsectionWithPath. If you want to document everything beneath purchase, you should add extra descriptors for purchase.id, purchase.pricingId, purchase.price, and purchase.type.