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.