Affects: Spring Boot 2.5.2

I'm using Spring WebFlux (with Annotations) and have, among other similar (working) controllers:

@Override
    public Flux<BulkDomainOperationResponse<Account>> syncAccounts(
            @RequestBody List<DomainOperation<Account>> operations,
            @PathVariable Instant from,
            @AuthenticationPrincipal
            Jwt jwt,
            ServerWebExchange exchange) {
        log.info("OPERATIONS_AFTER: " + operations);
        return commandHandler
                .syncAccounts(
                        new SyncAccountCommand(
                                operations,
                                from,
                                JwtParser.getUsername(jwt),
                                JwtParser.getRoles(jwt),
                                localeService.getLocale(exchange))
                );
    }

And a test using WebTestClient:

@Test
    @DisplayName("GIVEN /sync/from/{from} endpoint, "
               + "WHEN post request is made with multiple operations to process in bulk, "
               + "THEN a flux is returned with synced (including new) records")
    void testSync() {
        List<DomainOperation<Account>> operations = AccountFactory
                .createMultipleDomainOperations(10);
        log.info("OPERATIONS_BEFORE: " + operations);
        ParameterizedTypeReference<List<DomainOperation<Account>>> typeRef =
                new ParameterizedTypeReference<>() {};
        webClient.post()
                .uri("/account/accounts/sync/from/2021-01-01T00:00:01.000000Z")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .body(Mono.just(operations), typeRef)
                .header(HttpHeaders.AUTHORIZATION, "Bearer token")
                .header(HttpHeaders.ACCEPT_LANGUAGE, Locale.forLanguageTag("pt-PT").toString())
                .exchange()
                .expectStatus().isEqualTo(200)
                .expectBody()
                .jsonPath("$.length()").isEqualTo(10)
                .jsonPath("$[0].operation").isNotEmpty()
                .jsonPath("$[0].response").isNotEmpty()
                .jsonPath("$[0].exception").isEmpty()
                .jsonPath("$[0].response.statusCode").isEqualTo(201)
                .jsonPath("$[0].response.resourceObject").isNotEmpty()
                .jsonPath("$[0].response.resourceObject.name").isEqualTo("Account 1")
        ;
    }

The difference with other controllers resides in the body. In this case, it's a list with a parameterized type: List<DomainOperation<Account>>.

WebTestClient seems to extract the body to JSON but it won't convert it to the target POJO. I suspect it's because it's a nested parameterized type, not just List<DomainOperation> but List<DomainOperation<Account>>.

Logs from WebTestClient, including body content sent, before failing to convert it to the target POJOs:

2021-09-04 14:18:55.789 ERROR 55396 --- [           main] o.s.t.w.reactive.server.ExchangeResult   : Request details for assertion failure:

> POST /account/accounts/sync/from/2021-01-01T00:00:01.000000Z
> WebTestClient-Request-Id: [1]
> Content-Type: [application/json]
> Accept: [application/json]
> Authorization: [Bearer token]
> Accept-Language: [pt_PT]
> Content-Length: [6862]

//---> WHAT WAS SENT
[{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"66e97b4a-3205-44b4-8c37-6d4e7417a224","partyId":"208410e5-e57e-43f7-b3e3-4fedcb95f4d8","recordType":"account","nature":"account","name":"Account 1","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"0e4eb854-f6d2-4981-b8b7-cede7c590d4d","partyId":"efee517c-9577-4aa9-ac2a-0f577a839621","recordType":"account","nature":"account","name":"Account 2","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"85fcb614-dd59-4a28-8a2e-ec1ed855fb20","partyId":"a2cbc6d8-e6e2-403d-b0ba-9399ac3a0bda","recordType":"account","nature":"account","name":"Account 3","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"860c1492-f40b-4b70-a7db-f4ed8f0fe0d9","partyId":"590e9c15-eed2-4e0f-b573-84a8140bcb7b","recordType":"account","nature":"account","name":"Account 4","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"dbdc9965-afdd-451a-a12b-fb7ed4186179","partyId":"7d9acffb-f8bf-4031-9007-fdf08108aca6","recordType":"account","nature":"account","name":"Account 5","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"ff23d673-a6c5-4c76-92e6-8ff750b46669","partyId":"5161d3ee-ef98-4f5d-9c9d-dcc80227e8f3","recordType":"account","nature":"account","name":"Account 6","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"7ac373db-7228-430a-8402-112c08376de3","partyId":"79248cd0-bac3-4c7d-a6c1-1da1e48b6e3a","recordType":"account","nature":"account","name":"Account 7","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"86b2c51d-ac11-4fbc-935a-f206c7b0f606","partyId":"9eb76084-a44c-4a54-9144-dbc37aab0b11","recordType":"account","nature":"account","name":"Account 8","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"e2f20aa8-88c8-4816-b1fe-6b5fbc32edea","partyId":"af0057ba-1e0f-4d58-a7ac-1e5ec69bd8ed","recordType":"account","nature":"account","name":"Account 9","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}},{"operationType":"CREATE","item":{"createdBy":"createusername","createdDate":null,"lastModifiedBy":"username","lastModifiedDate":null,"lastSource":"L","isDeleted":false,"purgeDate":null,"isDuplicate":0,"etag":null,"allowedUsers":["username"],"id":"7cd7f75d-cd49-4c51-b1a6-1fbb44e95825","partyId":"c820c21d-909c-49fc-be81-e661473b0e68","recordType":"account","nature":"account","name":"Account 10","balance":{"amount":0.00,"currency":"USD","roundingMode":"HALF_EVEN","plus":false,"minus":false,"zero":true},"origin":"manual","status":"active","autoClearTransactions":true,"additionalInfo":"Just a test","useCheckbookRegister":false,"includeInNetWorth":true,"translations":[],"new":true}}]
//<--- WHAT WAS SENT

< 500 INTERNAL_SERVER_ERROR Internal Server Error
< Vary: [Origin, Access-Control-Request-Method, Access-Control-Request-Headers]
< Content-Type: [application/json]
< Content-Length: [387]
< Cache-Control: [no-cache, no-store, max-age=0, must-revalidate]
< Pragma: [no-cache]
< Expires: [0]
< X-Content-Type-Options: [nosniff]
< X-Frame-Options: [DENY]
< X-XSS-Protection: [1 ; mode=block]
< Referrer-Policy: [no-referrer]

{"timestamp":1630761535768,"path":"/account/accounts/sync/from/2021-01-01T00:00:01.000000Z","status":500,"error":"Internal Server Error","requestId":"b1319997","message":null,"exception":"NullPointerException","id":"3a68584a-72f6-4ff6-9b59-42fee04bb662","documentationUrl":"https://docs.thesaultymonk.com/docs/libraries/maven/tsm-spring-common/reference/exceptions/NullPointerException"}

An excerpt from my logs with what I send to the controller and what the controller actually receives:

//---> WHAT IS SENT TO WEBTESTCLIENT
2021-09-04 14:18:55.634  INFO 55396 --- [           main] 
c.t.s.c.a.in.web.rest.RestAdapterIT      : OPERATIONS_BEFORE: [DomainOperation(operationType=CREATE, item=Account(id=66e97b4a-3205-44b4-8c37-6d4e7417a224, partyId=208410e5-e57e-43f7-b3e3-4fedcb95f4d8, recordType=account, nature=ACCOUNT, name=Account 1, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=0e4eb854-f6d2-4981-b8b7-cede7c590d4d, partyId=efee517c-9577-4aa9-ac2a-0f577a839621, recordType=account, nature=ACCOUNT, name=Account 2, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=85fcb614-dd59-4a28-8a2e-ec1ed855fb20, partyId=a2cbc6d8-e6e2-403d-b0ba-9399ac3a0bda, recordType=account, nature=ACCOUNT, name=Account 3, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=860c1492-f40b-4b70-a7db-f4ed8f0fe0d9, partyId=590e9c15-eed2-4e0f-b573-84a8140bcb7b, recordType=account, nature=ACCOUNT, name=Account 4, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=dbdc9965-afdd-451a-a12b-fb7ed4186179, partyId=7d9acffb-f8bf-4031-9007-fdf08108aca6, recordType=account, nature=ACCOUNT, name=Account 5, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=ff23d673-a6c5-4c76-92e6-8ff750b46669, partyId=5161d3ee-ef98-4f5d-9c9d-dcc80227e8f3, recordType=account, nature=ACCOUNT, name=Account 6, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=7ac373db-7228-430a-8402-112c08376de3, partyId=79248cd0-bac3-4c7d-a6c1-1da1e48b6e3a, recordType=account, nature=ACCOUNT, name=Account 7, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=86b2c51d-ac11-4fbc-935a-f206c7b0f606, partyId=9eb76084-a44c-4a54-9144-dbc37aab0b11, recordType=account, nature=ACCOUNT, name=Account 8, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=e2f20aa8-88c8-4816-b1fe-6b5fbc32edea, partyId=af0057ba-1e0f-4d58-a7ac-1e5ec69bd8ed, recordType=account, nature=ACCOUNT, name=Account 9, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[])), DomainOperation(operationType=CREATE, item=Account(id=7cd7f75d-cd49-4c51-b1a6-1fbb44e95825, partyId=c820c21d-909c-49fc-be81-e661473b0e68, recordType=account, nature=ACCOUNT, name=Account 10, balance=$0.00, origin=MANUAL, status=ACTIVE, autoClearTransactions=true, additionalInfo=Just a test, useCheckbookRegister=false, includeInNetWorth=true, translations=[]))]
2021-09-04 14:18:55.746  INFO 55396 --- [     parallel-6] 
//---> WHAT IS RECEIVED BY THE CONTROLLER
s.c.s.a.i.w.r.ReactiveAccountRestAdapter : OPERATIONS_AFTER: [DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null), DomainOperation(operationType=null, item=null)]
2021-09-04 14:18:55.773 ERROR 55396 --- [     parallel-6] a.w.r.e.AbstractErrorWebExceptionHandler : [b1319997]  500 Server Error for HTTP POST "/account/accounts/sync/from/2021-01-01T00:00:01.000000Z"

As you can see, it parses List<DomainOperation> but it can't parse the level below.

Any ideas on how to overcome this problem?

Note: I have also created a Stack Overflow issue here.

Comment From: luissalgadofreire

It had to be something else much simpler.

I had forgotten to remove a @JsonView({Views.CreateRequest.class}) annotation from the @RequestBody List<DomainOperation<Account>> operations argument in the controller.

That was preventing the serialization of the inner objects.

Solved.