Is there a way to provide a custom error message depending on the given condition?
I'm using https://github.com/networknt/json-schema-validator, version 1.0.43
This is my JSON Schema:
{
"$id": "https://configurations/provider.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Configuration",
"type": "object",
"properties": {
"provider": {
"description": "Name of the provider.",
"enum": [
"Provider #1",
"Provider #2"
]
},
"configuration": {
"$schema": "json-schema/configurations/configuration.json"
}
},
"if": {
"properties": {
"parcsProvider": {
"const": "Provider #1"
}
}
},
"then": {
"required": [
"configuration"
]
},
"else": {
"not": {
"required": [
"configuration"
]
}
}
}
If the value for the provider is "Provider #1" then the configuration object is required, and if it's "Provider #2" and configuration is passed an error will occur. I want to customize that error so that the response is the same as it is now but with a custom message like "Provider 2 can't have a configuration."
Current error message/response:
{
"timestamp": "2020-11-23T12:50:56.20658+01:00",
"message": "invalid.json.input",
"validationErrors": [
{
"field": "$",
"message": "$: should not be valid to the schema \"not\" : {\"required\":[\"configuration\"]}"
}
]
}
I had a similar requirement to implement in one of my projects. For validation, I was using https://github.com/everit-org/json-schema.
Here is what I did
Categorized all kind of errors[there must be some specific keyword] thrown by the validator
Now once you have all the keys, you can easily manipulate the errors and send the custom error/response.
Below are the keys I have collected for different cases, this might help you -
MIN_LENGTH_VIOLATION = "expected minLength"
MAX_LENGTH_VIOLATION = "expected maxLength"
PATTERN_VIOLATION = "does not match pattern"
DATA_TYPE_VIOLATION = "expected type"
DEPENDENCY_VIOLATION = "is required"
FORMAT_VIOLATION_OR_ENUM_VALIDATION_VIOLATION = "is not a valid"
MANDATORY_FIELD_VIOLATION_OR_CONDITIONAL_VIOLATION = "required key"
NUMBER_IS_LESS_THAN_VIOLATION = "is not greater or equal to"
NUMBER_IS_GREATER_THAN_VIOLATION = "is not less or equal"
EXCLUSIVE_NUMBER_IS_GREATER_THAN_VIOLATION = "is not less than"
EXCLUSIVE_NUMBER_IS_LESS_THAN_VIOLATION = "is not greater than"
MULTIPLE_OF_VIOLATION = "is not a multiple"
Sample Code -
private static void validate(JSONObject request) {
try {
// Schema, that might be fetched dynamically from some data source
JSONObject schema = new JSONObject();
Schema loadedSchema = SchemaLoader.load(schema);
loadedSchema.validate(request);
} catch (ValidationException ve) {
List<String> allErrorMessages = ve.getAllMessages();
List<String> mandatoryFields = parseMandatoryField(allErrorMessages);
if (CollectionUtils.isNotEmpty(mandatoryFields)) {
throw new MandFieldMissingExp(mandatoryFields);
} else {
List<String> invalidFields = parseInvalids(allErrorMessages);
throw new InvalidFieldExp(invalidFields);
}
}
}
private static List<String> parseMandatoryField(List<String> validationExceptionMessages) {
Set<String> mandatoryListSet = new HashSet<>();
validationExceptionMessages.forEach(errorMessage -> {
if (StringUtils.containsAny(errorMessage, MANDATORY_FIELD_VIOLATION_OR_CONDITIONAL_VIOLATION, DEPENDENCY_VIOLATION)) {
mandatoryListSet.add(StringUtils.substring(errorMessage, errorMessage.indexOf('[') + 1, errorMessage.indexOf(']')));
}
});
return new ArrayList<>(mandatoryListSet);
}
private static List<String> parseInvalids(List<String> validationExceptionMessages) {
Set<String> invalidParamsSet = new HashSet<>();
validationExceptionMessages.forEach(errorMessage -> {
if (StringUtils.containsAny(errorMessage, MIN_LENGTH_VIOLATION, MAX_LENGTH_VIOLATION, PATTERN_VIOLATION,
FORMAT_VIOLATION_OR_ENUM_VALIDATION_VIOLATION, DATA_TYPE_VIOLATION, NUMBER_IS_LESS_THAN_VIOLATION,
MULTIPLE_OF_VIOLATION, EXCLUSIVE_NUMBER_IS_GREATER_THAN_VIOLATION, NUMBER_IS_GREATER_THAN_VIOLATION
, EXCLUSIVE_NUMBER_IS_LESS_THAN_VIOLATION)) {
invalidParamsSet.add(StringUtils.substring(errorMessage, errorMessage.indexOf('/') + 1, errorMessage.indexOf(':')));
}
});
return new ArrayList<>(invalidParamsSet);
}
Hope it helps
Related
I am looking to generate an api that take different content type.
The problem I am facing is that if I run several time my application I have different output documentation
#RestController
public class MyRestController {
#Operation(summary = "GetMyData", operationId = "gettt",
responses = #ApiResponse(responseCode = "204", content = #Content(mediaType = "application/vnd.something")))
#GetMapping(produces = "application/vnd.something")
public ResponseEntity<Void> getSomethingElse() {
return noContent().build();
}
#GetMapping(produces = TEXT_PLAIN_VALUE)
public String get() {
return "some text";
}
#GetMapping(produces = HAL_JSON_VALUE)
public EntityModel<JsonResponse> getHal() {
return EntityModel.of(new JsonResponse(),
linkTo(MyRestController.class).slash("somelink").withSelfRel()
);
}
#GetMapping(produces = APPLICATION_JSON_VALUE)
public JsonResponse getJson() {
return new JsonResponse();
}
}
It currently generate a wrong api-docs
"operationId": "gettt_1_1_1",
"responses": {
"200": {
"content": {
"application/hal+json": {
"schema": {
"$ref": "#/components/schemas/EntityModelJsonResponse"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/JsonResponse"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"204": {
"content": {
"application/hal+json": {
"schema": {
"$ref": "#/components/schemas/EntityModelJsonResponse"
}
},
"application/vnd.something": {},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"description": "No Content"
}
},
If I restart my server without changing the code the following response is generated
"operationId": "gettt_1",
"responses": {
"200": {
"content": {
"application/hal+json": {
"schema": {
"$ref": "#/components/schemas/EntityModelJsonResponse"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/JsonResponse"
}
},
"text/plain": {
"schema": {
"type": "string"
}
}
},
"description": "OK"
},
"204": {
"content": {
"application/vnd.something": {}
},
"description": "No Content"
}
},
I would expect that restarting my server will always generate the same documentation
Have you looked at the documentation?
https://springdoc.github.io/springdoc-openapi-demos/springdoc-properties.html#swagger-ui-properties
You can use the swagger-ui properties, without having to override the standard way of sorting (operationsSorter and tagsSorter).
For example:
springdoc.swagger-ui.operationsSorter=method
springdoc.swagger-ui.tagsSorter=alpha
If you want a an order on the server side, you can use OpenApiCustomiser, to sort the elements
This is a sample code that you can customize using Comparators, depending on the sorting logic you want:
Example, for alphabetical order sorting of schemas:
#Bean
public OpenApiCustomiser sortSchemasAlphabetically() {
return openApi -> {
Map<String, Schema> schemas = openApi.getComponents().getSchemas();
openApi.getComponents().setSchemas(new TreeMap<>(schemas));
};
}
Example for sorting tags, in alphabetical order:
#Bean
public OpenApiCustomiser sortTagsAlphabetically() {
return openApi -> openApi.setTags(openApi.getTags()
.stream()
.sorted(Comparator.comparing(tag -> StringUtils.stripAccents(tag.getName())))
.collect(Collectors.toList()));
}
You can have full control on the elements order, and you can sort them depending on your use case...
one other flag mentioned here:
springdoc:
writer-with-order-by-keys
I'm developing a simple api with GAE standard and Cloud Endpoint to test the available quota feature.
Here's the Java Code using the EndPoint Framework annotation:
package com.example.skeleton;
import com.google.api.server.spi.config.*;
#Api(
name = "simpleapi",
version = "v1",
limitDefinitions = {
#ApiLimitMetric(
name = "read-requests",
displayName = "Read requests",
limit = 10
)
}
)
public class MyApi {
#ApiMethod(
name = "hellosecuredWithQuotas",
path = "hellosecuredWithQuotas",
httpMethod = ApiMethod.HttpMethod.GET,
apiKeyRequired= AnnotationBoolean.TRUE,
metricCosts = {
#ApiMetricCost(
name ="read-requests",
cost = 1
)
}
)
public Message helloSecuredWithQuotas(#Named("name") String name) {
return new Message("hello " + name);
}
}
So given the #Api annotations the quotas is 10 requests per minute.
I deploy the App in GAE.
I Generate the OpenAPI json file (see below for the generated content) and deploy it to Cloud Endpoint using gcloud CLI.
Finally I use the generated client to call the endpoint in a loop which is calling the endpoint more than 10 times per minute.
... but unfortunately I never receive the expected "HTTP status code of 429 Too Many Requests".
public class App {
public static void main(String []args) throws IOException {
HttpTransport httpTransport = new NetHttpTransport();
JsonFactory jsonFactory = new JacksonFactory();
Simpleapi simpleapi = new Simpleapi.Builder(httpTransport, jsonFactory, null)
.setApplicationName("test")
.build();
for (int i = 0; i < 1000 ; i++) {
Message titi = simpleapi.hellosecuredWithQuotas("foobar" + System.currentTimeMillis()).setKey("my-api-key-here").execute();
System.out.println(titi.getMessage());
}
}
}
Here is the generated openapi.json file:
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "my-project-name-here.appspot.com"
},
"host": "my-project-name-here.appspot.com",
"basePath": "/_ah/api",
"schemes": [
"https"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/simpleapi/v1/hellosecuredWithQuotas": {
"get": {
"operationId": "SimpleapiHelloSecuredWithQuotas",
"parameters": [
{
"name": "name",
"in": "query",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "A successful response",
"schema": {
"$ref": "#/definitions/Message"
}
}
},
"security": [
{
"api_key": []
}
],
"x-google-quota": {
"metricCosts": {
"read-requests": 10
}
}
}
}
},
"securityDefinitions": {
"api_key": {
"type": "apiKey",
"name": "key",
"in": "query"
}
},
"definitions": {
"Message": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
},
"x-google-management": {
"metrics": [
{
"name": "read-requests",
"valueType": "INT64",
"metricKind": "GAUGE"
}
],
"quota": {
"limits": [
{
"name": "read-requests",
"metric": "read-requests",
"values": {
"STANDARD": 10
},
"unit": "1/min/{project}",
"displayName": "Read requests"
}
]
}
}
}
I was able to get this to work by setting the limit value in the ApiLimitMetric configuration to a value 100 or greater. If you'd like to have a maximum of 10 requests per minute, you should set the limit to 100 and the cost value in the ApiMetricCost configuration on the method to 10. This would give you the same ratio of 1/10 as listed in your original post.
(clarification copied from a comment)
I have a java.util.Map that has different key value pairs, and some of the values are dates, some numbers, some are strings, and some are also java.util.Maps that can also contain all kinds of above mentioned types. I am able to put it into the index, I see that the elasticsearch mapping is created automatically with correct field types, now I want to retrieve that Map and see dates, numbers, strings and nested Maps instead of what I currently have - just strings and Maps
Further story:
I'm putting a java.util.Map in Elasticsearch using the following code:
public void putMap(String key, Map<String, ?> value) {
try {
IndexRequest ir = Requests.indexRequest(_index)
.id(key)
.type("doc")
.source(value);
Factory.DB.index(ir); // the high level rest client here
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
I am not able to create mappings explicitly as per my task.
For one of my indices it has created the mapping like this, which is quite fine:
{
"rex": {
"mappings": {
"doc": {
"properties": {
"variables": {
"properties": {
"customer": {
"properties": {
"accounts": {
"properties": {
"dateOpen": {
"type": "date"
},
"id": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
},
"dateOfBirth": {
"type": "date"
},
"firstName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"id": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"lastName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
},
"middleName": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"ignore_above": 256
}
}
}
}
}
}
}
}
}
}
}
}
Now I am retrieving my structure back with the following code,
public Map<String, ?> getMap(String key) {
try {
GetRequest gr = new GetRequest(_index, "doc", key);
try {
GetResponse response = Factory.DB.get(gr);
if (!response.isExists()) {
return null;
}
Map<String, ?> ret = response.getSourceAsMap();
return ret;
} catch (ElasticsearchStatusException ee) {
if (ee.status().getStatus() == RestStatus.NOT_FOUND.getStatus()) {
return null;
} else {
throw new RuntimeException(ee);
}
}
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
The dates are returned as strings like "1957-04-29T00:00:00.000Z"
There's no Java object to map this document to as I have only Maps of Maps/Lists/values.
How do I make the Java Rest Client respect the mapping the Elasticsearch created for the index? response.getFields() returns empty map.
In case it is impossible (like 'source is json/strings by design' etc etc), I am ready to retrieve the mapping in the most convenient form possible and walk through the result by myself. The code to retrieve elasticsearch mapping will be appreciated.
Big thank you!
If you still want to fetch the type mapping and do the conversion manually, the Indices API has a Get Mapping command you can invoke with the Java Low Level REST Client.
String getMapping(RestHighLevelClient client, String index, String type) throws IOException {
String endpoint = index + "/_mapping/" + type;
Response response = client.getLowLevelClient().performRequest("GET", endpoint);
return EntityUtils.toString(response.getEntity());
}
But really I would recommend instead using something like Jackson for data binding. Bind the Map you get from Elasticsearch to a Java object that models the document, and let Jackson handle the type conversion for you.
First post, so sorry if my terminology is unclear.
So I'm using retrofit to GET an array of objects from a link: BASE_URL/todos?userId=1.
When using:
#GET("todos?userId={userId}")
Call<List<ToDoObject>> listTodos(#Query("userId") int userId);
for this retrofit call:
retrofit.Retrofit retrofit = new retrofit.Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
BaseServiceAPI service = retrofit.create(BaseServiceAPI.class);
Call<List<ToDoObject>> listToDos = service.listTodos(getUserId());
listToDos.enqueue(new Callback<List<ToDoObject>>() {
#Override
public void onResponse(Response<List<ToDoObject>> response, retrofit.Retrofit retrofit) {
if (response.body() != null) {
for (int i = 0; i < response.body().size(); i++) {
toDoObjectList.add(response.body().get(i));
}
toDoRecycAdapter.notifyDataSetChanged();
}else Log.d("flow", "no go");
}
#Override
public void onFailure(Throwable t) {
Log.d("flow", "todo failure: " + t.getMessage());
}
});
to get this array:
[
{
"completed": false,
"dueDate": "2016-07-31 06:38:00",
"id": 1,
"title": "Find your son Duke",
"userId": 1
},
{
"completed": false,
"dueDate": "2017-05-24 07:30:00",
"id": 4,
"title": "Rule the City ",
"userId": 1
},
{
"completed": true,
"dueDate": "2016-1-30 1:15:00",
"id": 6,
"title": "Run The ",
"userId": 1
},
{
"completed": true,
"dueDate": "2016-1-30 1:28:00",
"id": 7,
"title": "Hmmm",
"userId": 1
}
]
I receive this error: "FATAL EXCEPTION: Caused by: java.lang.IllegalArgumentException: URL query string "userId={userId}" must not have replace block. For dynamic query parameters use #Query."
I thought this was the proper case to use Query. I even tried Path but still nothing. I don't know many other programmers to ask and I've been at this for weeks. Can someone tell me what I'm doing wrong please??
Thanks in advance
You should just leave #Query parameter in your request and remove it from #GET. Also you say that you need to request only specific object, so why your response contains the list Call<List<ToDoObject>>?
#GET("todos")
Call<ToDoObject> specificTodo(#Query("userId") int userId);
I'm trying to deserialize a JSON object (from JIRA REST API createMeta) with unknown keys.
{
"expand": "projects",
"projects": [
{
"self": "http://www.example.com/jira/rest/api/2/project/EX",
"id": "10000",
"key": "EX",
"name": "Example Project",
"avatarUrls": {
"24x24": "http://www.example.com/jira/secure/projectavatar?size=small&pid=10000&avatarId=10011",
"16x16": "http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000&avatarId=10011",
"32x32": "http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000&avatarId=10011",
"48x48": "http://www.example.com/jira/secure/projectavatar?pid=10000&avatarId=10011"
},
"issuetypes": [
{
"self": "http://www.example.com/jira/rest/api/2/issueType/1",
"id": "1",
"description": "An error in the code",
"iconUrl": "http://www.example.com/jira/images/icons/issuetypes/bug.png",
"name": "Bug",
"subtask": false,
"fields": {
"issuetype": {
"required": true,
"name": "Issue Type",
"hasDefaultValue": false,
"operations": [
"set"
]
}
}
}
]
}
]
}
My problem is: I don't know the keys into "fields" (in the example below "issuetype", "summary", "description", "customfield_12345").
"fields": {
"issuetype": { ... },
"summary": { ... },
"description": { ... },
"customfield_12345": { ... }
}
It would be awesome if I could deserialize it as an array with the key as "id" in my POJO so the above example will looke like the following:
class IssueType {
...
public List<Field> fields;
...
}
class Field {
public String id; // the key from the JSON object e.g. "issuetype"
public boolean required;
public String name;
...
}
Is there a way I can achieve this and wrap in my model? I hope my problem is somehow understandable :)
If you don't know the keys beforehand, you can't define the appropriate fields. The best you can do is use a Map<String,Object>.
If there are in fact a handful of types, for which you can identify a collection of fields, you could write a custom deserializer to inspect the fields and return an object of the appropriate type.
I know it's old question but I also had problem with this and there are results..
Meybe will help someone in future : )
My Response with unknow keys:
in Model Class
private JsonElement attributes;
"attributes": {
"16": [],
"24": {
"165": "50000 H",
"166": "900 lm",
"167": "b.neutr.",
"168": "SMD 3528",
"169": "G 13",
"170": "10 W",
"171": "230V AC / 50Hz"
}
},
So I also checked if jsonElement is jsonArray its empty.
If is jsonObject we have data.
ProductModel productModel = productModels.get(position);
TreeMap<String, String> attrsHashMap = new TreeMap<>();
if (productModel.getAttributes().isJsonObject())
{
for (Map.Entry<String,JsonElement> entry : productModel.getAttributes().getAsJsonObject().entrySet())
{
Log.e("KEYS", "KEYS: " + entry.getKey() + " is empty: " + entry.getValue().isJsonArray());
if (entry.getValue() != null && entry.getValue().isJsonObject())
{
for (Map.Entry<String, JsonElement> entry1 : entry.getValue().getAsJsonObject().entrySet())
{
Log.e("KEYS", "KEYS INSIDE: " + entry1.getKey() + " VALUE: " + entry1.getValue().getAsString());
// and there is my keys and values.. in your case You can get it in upper for loop..
}
}
}
There is a perfectly adequate JSON library for Java that will convert any valid JSON into Java POJOs. http://www.json.org/java/