I am doing documentation for a REST service that returns this kind of JSON:
{
"CKM_RSA_PKCS_OAEP" : {
"keyType" : "RSA",
"canGenerateKey" : false,
"canEncryptDecrypt" : true,
"canSignVerify" : false,
"canWrapSecretKeys" : true,
"canWrapPrivateKeys" : false,
"canDerive" : false,
"canDigest" : false,
"minKeySize" : 512,
"maxKeySize" : 16384
},
"CKM_SHA384" : {
"canGenerateKey" : false,
"canEncryptDecrypt" : false,
"canSignVerify" : false,
"canWrapSecretKeys" : false,
"canWrapPrivateKeys" : false,
"canDerive" : false,
"canDigest" : true,
"minKeySize" : 0,
"maxKeySize" : 0
}
}
This represents a Map<Mechanism, MechanismInfo> where Mechanism is an enum and MechanismInfo a POJO.
I want to document only the fields of the MechanismInfo class regardless of Mechanism with the constraint that keyType attribute can be null and then not present in the response. All other attributes are either integers or boolean and will have a default value.
Here are a part of my Junit 5 test:
this.webTestClient = WebTestClient.bindToApplicationContext(applicationContext)
.configureClient()
.filter(documentationConfiguration(restDocumentation)
.operationPreprocessors()
.withResponseDefaults(prettyPrint()))
.build();
(...)
webTestClient
.get().uri("/tokens/{id}/mechanisms", TOKEN_TEST)
.exchange()
.expectStatus().isOk()
.expectBody(new ParameterizedTypeReference<Map<Mechanism, MechanismInfo>>() {
})
.consumeWith(document(
"get_mechanisms"
));
Could someone help me to do this?
As i finally found how to address this, I post how I did.
The main point is the modification of the content of the request before its documentation as described in Spring REST Docs documentation.
So, I created my own OperationPreprocessor to modify the response, taking advantage of the built-in ContentModifyingOperationPreprocessor which requires only to implement the ContentModifier interface. The idea was to be able to extract a unique value that I knew it was always there.
Here is what it looks like:
private final class MechanismsExtract implements ContentModifier {
#Override
public byte[] modifyContent(byte[] originalContent, MediaType contentType) {
ObjectMapper objectMapper = new ObjectMapper();
try {
Map<String, LinkedHashMap> map = objectMapper.readValue(originalContent, Map.class);
final LinkedHashMap mechanismInfo = map.entrySet().stream()
.filter(
key -> key.getKey().equals(Mechanism.CKM_RSA_PKCS_KEY_PAIR_GEN.name())
)
.map(
Map.Entry::getValue
)
.findFirst()
.orElseThrow();
return objectMapper.writeValueAsBytes(mechanismInfo);
} catch (IOException ex) {
return originalContent;
}
}
}
then when documenting:
(...)
.consumeWith(document(
"get_mechanisms",
// Output is limited to chosen mechanisms for documentation readability reasons
Preprocessors.preprocessResponse(
new ContentModifyingOperationPreprocessor(
new MechanismsExtract()
)
),
relaxedResponseFields(
fieldWithPath(NAME).description(DESCRIPTION),
(...)
)
(...)
)
(...)
Related
I have a UserDto with related items taken from repository which is Project-Reactor based, thus returns Flux/Mono publishers.
My idea was to add fields/getters to DTO, which themselves are publishers and lazily evaluate them (subscribe) on demand, but there is a problem:
Controller returns Flux of DTOs, all is fine, except spring doesn't serialize inner Publishers
What I'm trying to achieve in short:
#Repository
class RelatedItemsRepo {
static Flux<Integer> findAll() {
// simulates Flux of User related data (e.g. Orders or Articles)
return Flux.just(1, 2, 3);
}
}
#Component
class UserDto {
// Trying to get related items as field
Flux<Integer> relatedItemsAsField = RelatedItemsRepo.findAll();
// And as a getter
#JsonProperty("related_items_as_method")
Flux<Integer> relatedItemsAsMethod() {
return RelatedItemsRepo.findAll();
}
// Here was suggestion to collect flux to list and return Mono
// but unfortunately it doesn't make the trick
#JsonProperty("related_items_collected_to_list")
Mono<List<Integer>> relatedItemsAsList() {
return RelatedItemsRepo.findAll().collectList();
}
// .. another user data
}
#RestController
#RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public class MyController {
#GetMapping
Flux<UserDto> dtoFlux() {
return Flux.just(new UserDto(), new UserDto(), new UserDto());
}
}
And this is the response I get:
HTTP/1.1 200 OK
Content-Type: application/json
transfer-encoding: chunked
[
{
"related_items_as_method": {
"prefetch": -1,
"scanAvailable": true
},
"related_items_collected_to_list": {
"scanAvailable": true
}
},
{
"related_items_as_method": {
"prefetch": -1,
"scanAvailable": true
},
"related_items_collected_to_list": {
"scanAvailable": true
}
},
{
"related_items_as_method": {
"prefetch": -1,
"scanAvailable": true
},
"related_items_collected_to_list": {
"scanAvailable": true
}
}
]
It seems like Jackson doesn't serialize Flux properly and just calls .toString() on it (or something similar).
My question is: Is there existing Jackson serializers for Reactor Publishers or should I implement my own, or maybe am I doing something conceptually wrong.
So in short: how can I push Spring to evaluate those fields (subscribe to them)
If I understand correctly, what you try to achieve is to create an API that needs to respond with the following response:
HTTP 200
[
{
"relatedItemsAsField": [1,2,3]
},
{
"relatedItemsAsField": [1,2,3]
},
{
"relatedItemsAsField": [1,2,3]
}
]
I would collect all the elements emitted by the Flux generated by RelatedItemsRepo#findAll by using Flux#collectList, then map this to set the UserDto object as required.
Here is a gist.
I have an Array field replies in my document, which may or may not exist. If it exists, I want to return its size otherwise return 0. Below is my projection code. For documents that contain replies field with elements in it, it is returning 0.
ProjectionOperation project = Aggregation.project("title", "datePosted", "likes").
and(ConditionalOperators.ifNull(ArrayOperators.arrayOf("replies").length()).then(0)).as("repliesCount");
Reading the MongoDB reference doc, it is pretty straight forward but somehow it is not working.
Any suggestions are welcome.
Sample Data
{
"_id" : ObjectId("5e9e3873d022d154c54c2969"),
"title" : "Is this a nice place to meet some interesting people?",
"datePosted" : ISODate("2020-04-21T00:04:03.731Z"),
"views" : 0,
"likes" : 0,
"active" : true
},
{
"_id" : ObjectId("5e9e2f37d022d154c54c2961"),
"title" : "I am posting a quick discussion",
"datePosted" : ISODate("2020-04-20T23:24:39.768Z"),
"views" : 120,
"likes" : 4,
"active" : true,
"replies" : [
{
"_id" : ObjectId("5e9e2f69d022d154c54c2963"),
"reply" : "This is the first reply",
"datePosted" : ISODate("2020-04-20T23:25:29.608Z"),
"likes" : 0
},
{
"_id" : ObjectId("5e9e2f69d022ad4c54c2964"),
"reply" : "This is another reply",
"datePosted" : ISODate("2020-04-20T23:25:29.608Z"),
"likes" : 0
}
]
}
Root Cause
The solution provided by #valijon is correct. The problem lies in the POJO DiscussionDoc to which the result is mapped.
#Document(collection = "discussions")
public class DiscussionDoc {
#Id
private ObjectId id;
private String title;
private LocalDateTime datePosted;
private int views;
private int likes;
#Transient
private int repliesCount;
private boolean active;
#Field(value = "replies")
private List<ReplyDoc> replyList;
}
Removing #Transient it works. I am using #Transient on repliesCount because I do not want to persist this field when persisting a DiscussionDoc. There is no usefulness of persisting this field. But it will be used to hold total reply count when fetching a DiscussionDoc. But #Transient is somehow not letting repliesCount be set when a DiscussionDoc is fetched. I have removed #Transient to fix the problem. But what to do about repliesCount field now being persisted in DB?
You were almost close:
ProjectionOperation project = Aggregation.project("title", "datePosted", "likes").
and(ArrayOperators.arrayOf(ConditionalOperators.ifNull("replies").then(Collections.emptyList())).length()).as("repliesCount");
Equivalent to:
{
"$project": {
"title": 1,
"datePosted": 1,
"likes": 1,
"repliesCount": {
"$size": {
"$ifNull": [
"$replies",
[]
]
}
}
}
}
Workaround: You may use Gson serializer to get this calculated value:
private static final Gson g = new GsonBuilder().create();
...
List<Document> result = mongoTemplate.aggregate(aggregation,
mongoTemplate.getCollectionName(DiscussionDoc.class), Document.class)
.getMappedResults();
return result.stream()
.map(doc -> g.fromJson(doc.toJson(), DiscussionDoc.class))
.collect(Collectors.toList());
I have a legacy spring code where they use ModelAndView and they add the objects to it as below.
ModelAndView result = new ModelAndView();
result.addObject("folders", folders);
return result;
for the above i am getting response as
{
"folders": [
{
"recordCount": 0,
"folderContentType": "Reports",
"folderId": 34,
},
{
"recordCount": 2,
"folderContentType": "SharedReports",
"folderId": 88,
}
]
}
I have changed these to use Spring's RestController with a POJO backing the results returned from DB.
#GetMapping("/folders")
public List<Folder> getAllFolders() {
return Service.findAllFolders(1,2);
}
This returns a JSON as below
[
{
"folderId": 359056,
"folderName": "BE Shared Report Inbox",
"folderDescription": "BE Shared Report Inbox",
},
{
"folderId": 359057,
"folderName": "BE Shared Spec Inbox",
}]
How could i return this as exactly as my legacy code response. I know i can convert the List to Map and display. But, is there any equivalent
way.
Thanks.
You can put your result into a map.
#GetMapping("/folders")
public List<Folder> getAllFolders() {
return Service.findAllFolders(1,2);
}
Change to:
#GetMapping("/folders")
public Map<String,List<Folder>> getAllFolders() {
Map<String,List<Folder>> map = new HashMap<>();
map.put("folders",Service.findAllFolders(1,2));
return map;
}
Currently I'm using SpringData to build my restful project.
I'm using Page findAll(Pageable pageable, X condition, String... columns); ,this method .The result looks like this:
{
"content": [
{
"id": 2,
"ouId": 1,
"pClassId": 3,
"isPublic": 0,
"accessMethod": 3,
"modifierName": null
}
],
"last": true,
"totalPages": 1,
"totalElements": 3,
"number": 0,
"size": 10,
"sort": [
{
"direction": "DESC",
"property": "id",
"ignoreCase": false,
"nullHandling": "NATIVE",
"ascending": false,
"descending": true
}
],
"first": true,
"numberOfElements": 3
}
The question is how to hide some specific json field in the content ?
And #JsonIgnore annotation is not flexible , the fields I need in different APIs are different.
I tried to write an annotation ,but when processing the Page ,I found that the content is unmodifiable .
So , hope that someone could help me with it.
If you don't want to put annotations on your Pojos you can also use Genson.
Here is how you can exclude a field with it without any annotations (you can also use annotations if you want, but you have the choice).
Genson genson = new Genson.Builder().exclude("securityCode", User.class).create();
// and then
String json = genson.serialize(user);
OR using flexjson
import flexjson.JSONDeserializer;
import flexjson.JSONSerializer;
import flexjson.transformer.DateTransformer;
public String toJson(User entity) {
return new JSONSerializer().transform(new DateTransformer("MM/dd/yyyy HH:mm:ss"), java.util.Date.class)
.include("wantedField1","wantedField2")
.exclude("unwantedField1").serialize(entity);
}
You have to use a custom serialize like the following:
#JsonComponent
public class MovieSerializer extends JsonSerializer<Movie> {
#Override
public void serialize(Movie movie, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
// The basic information of a movie
jsonGenerator.writeNumberField("id", movie.getId());
jsonGenerator.writeStringField("name", movie.getName());
jsonGenerator.writeStringField("poster", movie.getPoster());
jsonGenerator.writeObjectField("releaseDate", movie.getReleaseDate());
jsonGenerator.writeObjectField("runtime", movie.getRuntime());
jsonGenerator.writeStringField("storyline", movie.getStoryline());
jsonGenerator.writeStringField("rated", movie.getRated());
jsonGenerator.writeNumberField("rating", movie.getRating());
jsonGenerator.writeEndObject();
}
}
And then annotate your model class with: #JsonSerialize(using = MovieSerializer.class)
I use #RequestBody #Valid annotations to validate JSON input argument for method in controller. It's web application serving API via spring-boot-starter-web 2.0.1.RELEASE.
When object validation finds error, default exception handler produces really nice JSON explaining what exactly is wrong with input.
The problem is that I have own #ExceptionHandler as my API has contract for failure messages. I want to incorporate Spring's validation JSON structure under my own JSON that would wrap it.
In my custom exception hanlder I see that I receieve MethodArgumentNotValidException but I don't know how to deserialize it to JSON the same way as Spring does internally. Apparently, in Spring there is some class that does it.
Could somebody give a hint?
Below example of JSON that I want to be part of my object
{
"timestamp" : "2018-05-01T22:45:28.907+0000",
"status" : 400,
"error" : "Bad Request",
"errors" : [{
"codes" : ["NotNull.ticket.items[0].amount", "NotNull.ticket.items.amount", "NotNull.items[0].amount", "NotNull.items.amount", "NotNull.amount", "NotNull.java.math.BigDecimal", "NotNull"],
"arguments" : [{
"codes" : ["ticket.items[0].amount", "items[0].amount"],
"arguments" : null,
"defaultMessage" : "items[0].amount",
"code" : "items[0].amount"
}
],
"defaultMessage" : "may not be null",
"objectName" : "ticket",
"field" : "items[0].amount",
"rejectedValue" : null,
"bindingFailure" : false,
"code" : "NotNull"
}
],
"message" : "Validation failed for object='ticket'. Error count: 1",
"path" : "/v1/databases/00000000-0000-0000-0000-000000000000/retail/pos/ticket"
}
I could not find a library method that solves this. I omitted rendering arguments as it's too much effort.
List<Map> validationErrors = new ArrayList<>();
BindingResult result = ((MethodArgumentNotValidException) e).getBindingResult();
for (FieldError error : result.getFieldErrors()) {
Map<String, Object> res = new HashMap<>();
res.put("codes", error.getCodes());
res.put("defaultMessage", error.getDefaultMessage());
res.put("objectName", error.getObjectName());
res.put("field", error.getField());
res.put("rejectedValue", error.getRejectedValue());
res.put("bindingFailure", error.isBindingFailure());
res.put("code", error.getCode());
validationErrors.add(res);
}
for (ObjectError error : result.getGlobalErrors()) {
Map<String, Object> res = new HashMap<>();
res.put("codes", error.getCodes());
res.put("defaultMessage", error.getDefaultMessage());
res.put("objectName", error.getObjectName());
res.put("code", error.getCode());
validationErrors.add(res);
}
// now attach validationErrors to object