Skip to content

Commit

Permalink
Support validation groups with @Validated
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin-wise committed Apr 28, 2022
1 parent 8d69c52 commit 4f4bf33
Show file tree
Hide file tree
Showing 17 changed files with 730 additions and 11 deletions.
@@ -0,0 +1,27 @@
You can enforce a subset of constraints using https://beanvalidation.org/2.0/spec/#validationapi-validatorapi-groups[validation groups] using `groups` on api:validation.Validated[]. More information is available in the https://beanvalidation.org/2.0/spec/#constraintdeclarationvalidationprocess-groupsequence[Bean Validation specification]

snippet::io.micronaut.docs.datavalidation.groups.FinalValidation[tags="clazz", indent=0]

<1> Define a custom validation group. This one extends `Default` so any validations done with this group will include constraints in the `Default` group.

snippet::io.micronaut.docs.datavalidation.groups.Email[tags="clazz", indent=0]

<1> Specify a constraint using the Default validation group. This constraint will only be enforced when `Default` is active.
<2> Specify a constraint using the custom `FinalValidation` validation group. This constraint will only be enforced when `FinalValidation` is active.

Annotate your controller with api:validation.Validated[], specifying the validation groups that will be active or letting it default to `Default`. Also annotate the binding POJO with `@Valid`.

snippet::io.micronaut.docs.datavalidation.groups.EmailController[tags="imports,clazz", indent=0,title="Example"]

<1> Annotating with api:validation.Validated[] without specifying groups means that the `Default` group will be active. Since this is defined on the class, it will apply to all methods.
<2> Constraints in the `Default` validation group will be enforced, inheriting from the class. The effect is that `@NotBlank` on `email.recipient` will not be enforced when this method is called.
<3> Specifying `groups` means that these validation groups will be enforced when this method is called. Note that `FinalValidation` extends `Default` so constraints from both groups will be enforced.
<4> Constraints in the `Default` and `FinalValidation` validation groups will be enforced, since `FinalValidation` extends `Default`. The effect is that both `@NotBlank` constraints in `email` will be enforced when this method is called.

Validation of POJOs using the default validation group is shown in the following test:

snippet::io.micronaut.docs.datavalidation.groups.EmailControllerSpec[tags="pojovalidateddefault",indent=0]

Validation of POJOs using the custom `FinalValidation` validation group is shown in the following test:

snippet::io.micronaut.docs.datavalidation.groups.EmailControllerSpec[tags="pojovalidatedfinal",indent=0]
10 changes: 6 additions & 4 deletions src/main/docs/guide/toc.yml
Expand Up @@ -90,7 +90,9 @@ httpServer:
bodyAnnotation: Using the @Body Annotation
reactiveResponses: Reactive Responses
jsonBinding: JSON Binding with Jackson
datavalidation: Data Validation
datavalidation:
title: Data Validation
validationGroups: Validation Groups
staticResources: Serving Static Resources
errorHandling:
title: Error Handling
Expand Down Expand Up @@ -287,14 +289,14 @@ i18n:
localizedMessageSource: Localized Message Source
appendix:
title: Appendices
architecture:
architecture:
title: Micronaut Architecture
compilerArch: Compiler
compilerArch: Compiler
annotationArch: Annotation Metadata
introspectionArch: Bean Introspections
iocArch: Bean Definitions
aopArch: AOP Proxies
containerArch: Application Context
containerArch: Application Context
httpServerArch: HTTP Server
faq: Frequently Asked Questions (FAQ)
usingsnapshots: Using Snapshots
Expand Down
@@ -0,0 +1,32 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.docs.datavalidation.groups

//tag::clazz[]
import io.micronaut.core.annotation.Introspected

import javax.validation.constraints.NotBlank

@Introspected
class Email {

@NotBlank // <1>
String subject

@NotBlank(groups = FinalValidation) // <2>
String recipient
}
//end::clazz[]
@@ -0,0 +1,46 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.docs.datavalidation.groups

import io.micronaut.context.annotation.Requires
//tag::imports[]
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated

import javax.validation.Valid
//end::imports[]

@Requires(property = "spec.name", value = "datavalidationgroups")
//tag::clazz[]
@Validated // <1>
@Controller("/email")
class EmailController {

@Post("/createDraft")
HttpResponse createDraft(@Body @Valid Email email) { // <2>
HttpResponse.ok(msg: "OK")
}

@Post("/send")
@Validated(groups = [FinalValidation]) // <3>
HttpResponse send(@Body @Valid Email email) { // <4>
HttpResponse.ok(msg: "OK")
}
}
//end::clazz[]
@@ -0,0 +1,79 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.docs.datavalidation.groups

import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class EmailControllerSpec extends Specification {

@Shared
@AutoCleanup
EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer,
['spec.name': 'datavalidationgroups'],
"test")

@Shared
@AutoCleanup
HttpClient client = embeddedServer.applicationContext.createBean(HttpClient, embeddedServer.URL)

//tag::pojovalidateddefault[]
def "invoking /email/createDraft parse parameters in a POJO and validates using default validation groups"() {
when:
Email email = new Email(subject: '', recipient: '')
client.toBlocking().exchange(HttpRequest.POST('/email/createDraft', email))

then:
def e = thrown(HttpClientResponseException)
def response = e.response
response.status == HttpStatus.BAD_REQUEST

when:
email = new Email(subject: 'Hi', recipient: '')
response = client.toBlocking().exchange(HttpRequest.POST('/email/createDraft', email))

then:
response.status == HttpStatus.OK
}
//end::pojovalidateddefault[]

//tag::pojovalidatedfinal[]
def "invoking /email/send parse parameters in a POJO and validates using FinalValidation validation group"() {
when:
Email email = new Email(subject: 'Hi', recipient: '')
client.toBlocking().exchange(HttpRequest.POST('/email/send', email))

then:
def e = thrown(HttpClientResponseException)
def response = e.response
response.status == HttpStatus.BAD_REQUEST

when:
email = new Email(subject: 'Hi', recipient: 'me@micronaut.example')
response = client.toBlocking().exchange(HttpRequest.POST('/email/send', email))

then:
response.status == HttpStatus.OK
}
//end::pojovalidatedfinal[]
}
@@ -0,0 +1,24 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.docs.datavalidation.groups;

//tag::clazz[]

import javax.validation.groups.Default

interface FinalValidation extends Default {} // <1>

//end::clazz[]
@@ -0,0 +1,31 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.docs.datavalidation.groups

//tag::clazz[]
import io.micronaut.core.annotation.Introspected
import javax.validation.constraints.NotBlank

@Introspected
open class Email {

@NotBlank // <1>
var subject: String? = null

@NotBlank(groups = [FinalValidation::class]) // <2>
var recipient: String? = null
}
//end::clazz[]
@@ -0,0 +1,45 @@
/*
* Copyright 2017-2020 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.docs.datavalidation.groups

import io.micronaut.context.annotation.Requires
//tag::imports[]
import io.micronaut.http.HttpResponse
import io.micronaut.http.annotation.Body
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Post
import io.micronaut.validation.Validated
import javax.validation.Valid
//end::imports[]

@Requires(property = "spec.name", value = "datavalidationgroups")
//tag::clazz[]
@Validated // <1>
@Controller("/email")
open class EmailController {

@Post("/createDraft")
open fun createDraft(@Body @Valid email: Email): HttpResponse<*> { // <2>
return HttpResponse.ok(mapOf("msg" to "OK"))
}

@Post("/send")
@Validated(groups = [FinalValidation::class]) // <3>
open fun send(@Body @Valid email: Email): HttpResponse<*> { // <4>
return HttpResponse.ok(mapOf("msg" to "OK"))
}
}
//end::clazz[]

0 comments on commit 4f4bf33

Please sign in to comment.