vertx-web icon indicating copy to clipboard operation
vertx-web copied to clipboard

Parameter name is missing from validation exception in open api.

Open javadevmtl opened this issue 4 years ago • 31 comments

Version

3.x vs 4.x

Context

Hi, in Vertx 3 when using open api, if we had a validation error in the Json we used to get back the name of the Json property that failed the validation.

It looks like in Vertx 4 vertx-web-openapi this information is gone?

In vertx 3 we could use ValidationException and get the parameter name with the function parameterName(). But now with vertx 4 BodyProcessorException we don't get that info.

We use that information to create proper error messages back to the client to explain which field had the issue.

Now we just get an error as follows: "[Bad Request] Validation error for body application/json; charset=UTF-8: provided string should respect pattern ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}"

Which isn't any more helpful cause it tells us the pattern failed but not for what paremeter.

javadevmtl avatar Sep 15 '21 22:09 javadevmtl

Hi, so just to be sure 2 things will happen 1- A function like parameterName() or similar will be added. 2- The error message will contain the field name.

javadevmtl avatar Sep 20 '21 14:09 javadevmtl

@InfoSec812 are you interested to handle this case ?

vietj avatar Oct 12 '21 13:10 vietj

@vietj I am interested in looking into this as I too have had this problem. The question is how quickly I will be able to address it. My schedule for the next few weeks is overflowing. It will likely be November before I can dig into this.

InfoSec812 avatar Oct 12 '21 13:10 InfoSec812

Thank you @InfoSec812

@vietj Given this is not a blocker, I believe it is fine to postpone to 4.2.1.

tsegismont avatar Oct 13 '21 04:10 tsegismont

I have started looking at this and it does not appear to be a problem with OpenAPI support. The actual root cause is in the validation module which is used by the OpenAPI components.

@javadevmtl Could you provide an example error with stack trace of what it looks like in Vert.x 3.x please? Then I can probably put together a PR and get this resolved pretty quickly.

InfoSec812 avatar Oct 16 '21 11:10 InfoSec812

Hi here is a sample stack trace. But I don't think you should only rely on 1-2 stack traces that I provided below. I also provided a sample code of how I handle the validation error to output to the response.

ValidationException{parameterName='body.domain', validationRule=null, value='{
    "domain": "foo.bar12345678901234567890",
    "url": "http://apple.com",
    "name": "First",
    "description": "Testing!",
    "expiresAt": "2121-10-15T00:00:00.12345Z"
}', message='$.domain: may only be 16 characters long', errorType=JSON_INVALID}
	at io.vertx.ext.web.api.validation.ValidationException$ValidationExceptionFactory.generateInvalidJsonBodyException(ValidationException.java:202)
	at io.vertx.ext.web.api.validation.impl.JsonTypeValidator.isValid(JsonTypeValidator.java:50)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.validateEntireBody(BaseValidationHandler.java:296)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.handle(BaseValidationHandler.java:82)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.handle(BaseValidationHandler.java:21)
	at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1038)
	at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:101)
	at io.vertx.ext.web.impl.RoutingContextWrapper.next(RoutingContextWrapper.java:176)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.lambda$authorizeUser$3(AuthHandlerImpl.java:223)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.authorize(AuthHandlerImpl.java:107)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.authorizeUser(AuthHandlerImpl.java:217)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.lambda$null$1(AuthHandlerImpl.java:161)
	at io.vertx.ext.auth.jwt.impl.JWTAuthProviderImpl.authenticate(JWTAuthProviderImpl.java:150)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.lambda$handle$2(AuthHandlerImpl.java:150)
	at io.vertx.ext.web.handler.impl.JWTAuthHandlerImpl.lambda$parseCredentials$0(JWTAuthHandlerImpl.java:76)
	at io.vertx.ext.web.handler.impl.AuthorizationAuthHandler.parseAuthorization(AuthorizationAuthHandler.java:97)
	at io.vertx.ext.web.handler.impl.JWTAuthHandlerImpl.parseCredentials(JWTAuthHandlerImpl.java:70)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.handle(AuthHandlerImpl.java:129)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.handle(AuthHandlerImpl.java:39)
	at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1038)
	at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:101)
	at io.vertx.ext.web.impl.RoutingContextWrapper.next(RoutingContextWrapper.java:176)
	at io.vertx.ext.web.handler.impl.ResponseContentTypeHandlerImpl.handle(ResponseContentTypeHandlerImpl.java:54)
	at io.vertx.ext.web.handler.impl.ResponseContentTypeHandlerImpl.handle(ResponseContentTypeHandlerImpl.java:28)
	at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1038)
	at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:137)
	at io.vertx.ext.web.impl.RoutingContextWrapper.next(RoutingContextWrapper.java:176)
	at io.vertx.ext.web.handler.impl.BodyHandlerImpl$BHandler.doEnd(BodyHandlerImpl.java:296)
	at io.vertx.ext.web.handler.impl.BodyHandlerImpl$BHandler.end(BodyHandlerImpl.java:276)
	at io.vertx.ext.web.handler.impl.BodyHandlerImpl.lambda$handle$0(BodyHandlerImpl.java:87)
	at io.vertx.core.http.impl.HttpServerRequestImpl.onEnd(HttpServerRequestImpl.java:525)
	at io.vertx.core.http.impl.HttpServerRequestImpl.handleEnd(HttpServerRequestImpl.java:511)
	at io.vertx.core.http.impl.Http1xServerConnection.handleEnd(Http1xServerConnection.java:176)
	at io.vertx.core.http.impl.Http1xServerConnection.handleContent(Http1xServerConnection.java:163)
	at io.vertx.core.http.impl.Http1xServerConnection.handleMessage(Http1xServerConnection.java:140)
	at io.vertx.core.impl.ContextImpl.executeTask(ContextImpl.java:366)
	at io.vertx.core.impl.EventLoopContext.execute(EventLoopContext.java:43)
	at io.vertx.core.impl.ContextImpl.executeFromIO(ContextImpl.java:229)
	at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:164)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:93)
	at io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandler.channelRead(WebSocketServerExtensionHandler.java:99)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.vertx.core.http.impl.Http1xOrH2CHandler.end(Http1xOrH2CHandler.java:61)
	at io.vertx.core.http.impl.Http1xOrH2CHandler.channelRead(Http1xOrH2CHandler.java:38)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:834)

One more example for required fields...

ValidationException{parameterName='body', validationRule=null, value='{
    "domain": "foo.bar",
    "url": "http://apple.com",
    "description": "Testing!",
    "expiresAt": "2121-10-15T00:00:00.12345Z"
}', message='$.name: is missing but it is required', errorType=JSON_INVALID}
	at io.vertx.ext.web.api.validation.ValidationException$ValidationExceptionFactory.generateInvalidJsonBodyException(ValidationException.java:202)
	at io.vertx.ext.web.api.validation.impl.JsonTypeValidator.isValid(JsonTypeValidator.java:50)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.validateEntireBody(BaseValidationHandler.java:296)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.handle(BaseValidationHandler.java:82)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.handle(BaseValidationHandler.java:21)
	at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1038)
	at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:101)
	at io.vertx.ext.web.impl.RoutingContextWrapper.next(RoutingContextWrapper.java:176)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.lambda$authorizeUser$3(AuthHandlerImpl.java:223)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.authorize(AuthHandlerImpl.java:107)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.authorizeUser(AuthHandlerImpl.java:217)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.lambda$null$1(AuthHandlerImpl.java:161)
	at io.vertx.ext.auth.jwt.impl.JWTAuthProviderImpl.authenticate(JWTAuthProviderImpl.java:150)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.lambda$handle$2(AuthHandlerImpl.java:150)
	at io.vertx.ext.web.handler.impl.JWTAuthHandlerImpl.lambda$parseCredentials$0(JWTAuthHandlerImpl.java:76)
	at io.vertx.ext.web.handler.impl.AuthorizationAuthHandler.parseAuthorization(AuthorizationAuthHandler.java:97)
	at io.vertx.ext.web.handler.impl.JWTAuthHandlerImpl.parseCredentials(JWTAuthHandlerImpl.java:70)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.handle(AuthHandlerImpl.java:129)
	at io.vertx.ext.web.handler.impl.AuthHandlerImpl.handle(AuthHandlerImpl.java:39)
	at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1038)
	at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:101)
	at io.vertx.ext.web.impl.RoutingContextWrapper.next(RoutingContextWrapper.java:176)
	at io.vertx.ext.web.handler.impl.ResponseContentTypeHandlerImpl.handle(ResponseContentTypeHandlerImpl.java:54)
	at io.vertx.ext.web.handler.impl.ResponseContentTypeHandlerImpl.handle(ResponseContentTypeHandlerImpl.java:28)
	at io.vertx.ext.web.impl.RouteState.handleContext(RouteState.java:1038)
	at io.vertx.ext.web.impl.RoutingContextImplBase.iterateNext(RoutingContextImplBase.java:137)
	at io.vertx.ext.web.impl.RoutingContextWrapper.next(RoutingContextWrapper.java:176)
	at io.vertx.ext.web.handler.impl.BodyHandlerImpl$BHandler.doEnd(BodyHandlerImpl.java:296)
	at io.vertx.ext.web.handler.impl.BodyHandlerImpl$BHandler.end(BodyHandlerImpl.java:276)
	at io.vertx.ext.web.handler.impl.BodyHandlerImpl.lambda$handle$0(BodyHandlerImpl.java:87)
	at io.vertx.core.http.impl.HttpServerRequestImpl.onEnd(HttpServerRequestImpl.java:525)
	at io.vertx.core.http.impl.HttpServerRequestImpl.handleEnd(HttpServerRequestImpl.java:511)
	at io.vertx.core.http.impl.Http1xServerConnection.handleEnd(Http1xServerConnection.java:176)
	at io.vertx.core.http.impl.Http1xServerConnection.handleContent(Http1xServerConnection.java:163)
	at io.vertx.core.http.impl.Http1xServerConnection.handleMessage(Http1xServerConnection.java:140)
	at io.vertx.core.impl.ContextImpl.executeTask(ContextImpl.java:366)
	at io.vertx.core.impl.EventLoopContext.execute(EventLoopContext.java:43)
	at io.vertx.core.impl.ContextImpl.executeFromIO(ContextImpl.java:229)
	at io.vertx.core.net.impl.VertxHandler.channelRead(VertxHandler.java:164)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.channel.ChannelInboundHandlerAdapter.channelRead(ChannelInboundHandlerAdapter.java:93)
	at io.netty.handler.codec.http.websocketx.extensions.WebSocketServerExtensionHandler.channelRead(WebSocketServerExtensionHandler.java:99)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:324)
	at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:296)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
	at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
	at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
	at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:719)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:655)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:581)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:834)

And this is how I handle it in Vertx.3

if (contextFailure instanceof ValidationException) {
            ValidationException ve = (ValidationException) contextFailure;

            ErrorType errorType = ve.type();
            switch(errorType) {
                case JSON_INVALID:
                case NO_MATCH:
                    String parameterName = ve.parameterName().startsWith("body") ? ve.parameterName() : "body." + ve.parameterName();
                    String validationMessage = parameterName + " is invalid.";

                    context.put("message", validationMessage);

                    badRequest(context, validationMessage);
                    break;
                default:
                    String message = "body is invalid.";
                    context.put("message", message);
                    badRequest(context, message);

                    break;
            }
        }

javadevmtl avatar Oct 18 '21 13:10 javadevmtl

@javadevmtl Thanks for the quick response. I will try to take a look later this week and at least provide you with a proof-of-concept to let you have a chance to validate.

InfoSec812 avatar Oct 18 '21 13:10 InfoSec812

Grrr... I can't get the current master branch to compile so that I can update the unit tests and try to work toward a solution.

InfoSec812 avatar Oct 23 '21 16:10 InfoSec812

I guess this is still ongoing?

I just tried 4.2.4 and still no field name is provided. It just tells us that there's a string somewhere wrong on the Json. But not which property of the json.

io.vertx.ext.web.validation.BodyProcessorException: [Bad Request] Validation error for body application/json; charset=UTF-8: provided string should respect pattern ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}

For query parameters ParameterProcessorException is adequate with getParameterName()

javadevmtl avatar Feb 08 '22 16:02 javadevmtl

@vietj Do you foresee this being fixed any time soon?

javadevmtl avatar May 19 '22 15:05 javadevmtl

Any update on this? I can't be the only one who needs this? lol

javadevmtl avatar Sep 06 '22 19:09 javadevmtl

@javadevmtl No updates... It's quite a bit more involved than I had originally expected. The new implementation to support JSONSchema (to handle OpenAPI 3.1) meant a much bigger change in the schema validation than I had expected. @pmlopes has done a great job getting the JSONSchema implementation compliant, but this work also means that there is no current way to bubble this information back up to provide the detailed exception you are looking for. Fixing this regression is going to be an extremely large amount of work. I don't know that any of us will be able to give you an estimate on if/when this might get resolved.

InfoSec812 avatar Sep 07 '22 02:09 InfoSec812

Yeah, I can't migrate from 3.9.X to 4 because of this. I'm I the only one who actually returns the field name as part of my error messages to the API clients?

Is there a work around?

Also can you point to where the validation happens in the OpenApi code I was curious to go look and see if I could maybe contribute. Last time I tried to look but got lost in the code.

javadevmtl avatar Sep 07 '22 14:09 javadevmtl

Yes am noticing this with our product testing as well. It would be very helpful to have that information provided again in the validation exception.

pantinor avatar Dec 08 '22 22:12 pantinor

Hi any news on this? I would really like to migrate from 3.9.x to 4.x

javadevmtl avatar Mar 14 '23 16:03 javadevmtl

cc @pmlopes @pk-work

tsegismont avatar Mar 24 '23 08:03 tsegismont

Hi, with Vert.x 4.4 we released a complete re-build of Vert.x OpenAPI [1]. Especially the error messages which are now generated are very helpful. They now look like this: The value of the request / response body is invalid. Reason: Instance type number is invalid. Expected integer at #/guest/friends/0/age

Or in case a header param is missing:

The related request / response does not contain the required header parameter p1.

But the APIs have changed, so you should expect some work to use the new libraries.

[1] https://vertx.io/docs/#standards

pk-work avatar Mar 24 '23 08:03 pk-work

Hi thats fine we already had to make changes to go from 3.x to 4.x Open-Api. This is why we opened this bug because the older 4.x changes removed "functionality".

Just want to make sure with 4.4 and the new error message do we have a way to get the name of the property that failed the validation? In 3.x we had ValidationException.getParameterName() is something similar available in 4.4? Or we have to parse the error message?

javadevmtl avatar Mar 24 '23 16:03 javadevmtl

For now I'm not convinced that a getParameterName() is beneficial in general, because if the "path" contains cycles the name of the property isn't enough to identify the error location.

So yes, for now you would have to parse the error message. Please check also getCause(). This will give you a JsonSchemaValidationException which has more details. Maybe it's easier for you to get relevant information from JsonSchemaValidationException instead of parsing the error messages.

pk-work avatar Mar 27 '23 07:03 pk-work

So far it seems we are 3 people that needed this @InfoSec812 @pantinor and myself (But it doesn't seem other people have had this issue???). I can't speak for the entire API world but for me it makes sense to be able to get the name of the problematic "parameter" as a function even if it's a json path like #/guest/friends/0/age. rather then trying to figure what the name is through these 2 texts:

The value of the request / response body is invalid. Reason: Instance type number is invalid. Expected integer at #/guest/friends/0/age.

The related request / response does not contain the required header parameter p1

I think parsing this would very brittle.

I would also hate to find out that I start migration again and find out that JsonSchemaValidationException isn't adequate.

javadevmtl avatar Mar 28 '23 15:03 javadevmtl

A function that returns the JSON Path / Pointer to the problematic property would make sense in my opinion,, but not only the "name".

pk-work avatar Mar 28 '23 15:03 pk-work

I need too I update my applications with new validators, and now i've problem to have my errors for consumers.

So, need, json pointers for field rejected value perhaps ?

So, with new validators, we need to update OuputUnit and JsonSchemaValidationException to do that ?

i'll try to see that on next week end

Fyro-Ing avatar Sep 17 '23 18:09 Fyro-Ing

Hi @InfoSec812 @javadevmtl @pantinor @Fyro-Ing @tsegismont ,

as I mentioned above, it is not that easy to identify the "parameter" which causes the issue, at least not if you want to have a general solution. I'd also like to have a "getParameter" or "getPointer" method, but that is not that simple.

I will share the following example with you and maybe together we will find a solution.

Example Schema:

components:
  schemas:
    Identifiable:
      type: object
      properties:
        id:
          type: string
      required:
        - id
    IntegerElement:
      allOf:
        - "$ref": "#/components/schemas/Identifiable"
        - properties:
            value:
              type: integer
            modelType:
              type: string
              pattern: IntegerElement
          required:
            - value
            - modelType
    StringElement:
      allOf:
        - "$ref": "#/components/schemas/Identifiable"
        - properties:
            value:
              type: string
            modelType:
              type: string
              pattern: StringElement
          required:
            - value
            - modelType
    ListElement:
      allOf:
        - "$ref": "#/components/schemas/Identifiable"
        - properties:
            value:
              type: array
              items:
                allOf:
                  - $ref: "#/components/schemas/ListElement_choice"
            modelType:
              type: string
              pattern: ListElement
          required:
            - value
            - modelType
    ListElement_choice:
      oneOf:
        - "$ref": "#/components/schemas/IntegerElement"
        - "$ref": "#/components/schemas/StringElement"
        - "$ref": "#/components/schemas/ListElement"
    List:
      type: array
      items:
        "$ref": "#/components/schemas/ListElement_choice"
      minItems: 1

Invalid Input:

[
  {
    "id": "integerElement1",
    "value": "ABC",
    "modelType": "IntegerElement"
  }
]

OutputUnit Error:

#/items: Items did not match schema
#/items/$ref: A subschema had errors
#/items/$ref/oneOf: Instance does not match exactly one subschema (0 matches)
#/items/$ref/oneOf/0/$ref: A subschema had errors
#/items/$ref/oneOf/0/$ref/allOf: Instance does not match every subschema
#/items/$ref/oneOf/0/$ref/allOf/1/properties: Property "value" does not match schema
#/items/$ref/oneOf/0/$ref/allOf/1/properties/value/type: Instance type string is invalid. Expected integer
#/items/$ref/oneOf/1/$ref: A subschema had errors
#/items/$ref/oneOf/1/$ref/allOf: Instance does not match every subschema
#/items/$ref/oneOf/1/$ref/allOf/1/properties: Property "modelType" does not match schema
#/items/$ref/oneOf/1/$ref/allOf/1/properties/modelType/pattern: String does not match pattern
#/items/$ref/oneOf/2/$ref: A subschema had errors
#/items/$ref/oneOf/2/$ref/allOf: Instance does not match every subschema
#/items/$ref/oneOf/2/$ref/allOf/1/properties: Property "value" does not match schema
#/items/$ref/oneOf/2/$ref/allOf/1/properties/value/type: Instance type string is invalid. Expected array
#/items/$ref/oneOf/2/$ref/allOf/1/properties: Property "modelType" does not match schema
#/items/$ref/oneOf/2/$ref/allOf/1/properties/modelType/pattern: String does not match pattern

The Problem

As you can see the error is very simple, the IntegerElement requires a integer value and not a String. So in the list of errors the OutputUnit provides, this line

#/items/$ref/oneOf/0/$ref/allOf/1/properties/value/type: Instance type string is invalid. Expected integer

is obviously the correct error and if I understood you correctly you want to have a method that returns this value: /items/$ref/oneOf/0/$ref/allOf/1/properties/value/type.

Now the question to you, how can an algorithm find out that exactly this is the related error? Otherwise I can't provide it in a method.

pk-work avatar Oct 25 '23 14:10 pk-work

@pk-work How does it work on vertx-3? In my opinion it should work like vertx-3 but separated into the new classes validators that where created for vertx-4.

Sample stack trace from vertx-3:

io.vertx.ext.web.api.validation.ValidationException: $.myFancyField: does not match the regex pattern ^\d+$
	at io.vertx.ext.web.api.validation.ValidationException$ValidationExceptionFactory.generateInvalidJsonBodyException(ValidationException.java:202)
	at io.vertx.ext.web.api.validation.impl.JsonTypeValidator.isValid(JsonTypeValidator.java:50)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.validateEntireBody(BaseValidationHandler.java:296)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.handle(BaseValidationHandler.java:82)
	at io.vertx.ext.web.api.validation.impl.BaseValidationHandler.handle(BaseValidationHandler.java:21)
...

And we do

ValidationException ve = (ValidationException)contextFailure; 
String validationMessage = ve.parameterName() + " is invalid."

So the message returned to the client in this case will print myFancyField is invalid.

We don't use nested field or really complexe Json. But if vertx-3 ve.parameterName() returns a full Json path then thats exactly what our code would print.

javadevmtl avatar Oct 25 '23 15:10 javadevmtl

How does it work on vertx-3?

I don't know exactly. But this what the new JSON Schema validation engine provides. Maybe it was possible, because the old engine wasn't able to do such complex schemas ...

As I mentioned in an earlier post you can access the "cause" and get more information. Maybe ((JsonSchemaValidationException) exception.cause).location() do already the job for you in case you have very simple schemas.

pk-work avatar Oct 25 '23 18:10 pk-work

Do you have example of what ((JsonSchemaValidationException) exception.cause).location() it would return?

javadevmtl avatar Oct 25 '23 18:10 javadevmtl

It will return the keywordLocation like #/items/$ref/oneOf/2/$ref/allOf/1/properties/modelType/pattern

pk-work avatar Oct 25 '23 19:10 pk-work

@pk-work And to be clear for my case where there's no nesting it will return $.myFancyField as is?

javadevmtl avatar Oct 26 '23 13:10 javadevmtl

It will return the "keywordLocation" of the first error from OutputUnit. If there is only one error, it will work, otherwise you need to call getCause() recursively.

Here [1] is the code that I'm using in vertx-openapi to generate the text message. I would suggest you create a reproducer with your schema and then we will see if it works or not.

[1] https://github.com/eclipse-vertx/vertx-openapi/blob/d0be35dbf9df2f69d69baef0b5e5e32b333e0b66/src/main/java/io/vertx/openapi/validation/ValidatorException.java#L93

pk-work avatar Oct 26 '23 13:10 pk-work

After some discussion, we believe that the best solution is to make the errors from the OutputUnit easily consumable. Then everyone can parse the necessary information from the errors.

pk-work avatar Nov 02 '23 08:11 pk-work