DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Enterprise AI Trend Report: Gain insights on ethical AI, MLOps, generative AI, large language models, and much more.

2024 Cloud survey: Share your insights on microservices, containers, K8s, CI/CD, and DevOps (+ enter a $750 raffle!) for our Trend Reports.

PostgreSQL: Learn about the open-source RDBMS' advanced capabilities, core components, common commands and functions, and general DBA tasks.

AI Automation Essentials. Check out the latest Refcard on all things AI automation, including model training, data security, and more.

Related

  • New ORM Framework for Kotlin
  • Testcontainers With Kotlin and Spring Data R2DBC
  • Automated CI/CD of Multiple Projects Using TeamCity’s Kotlin DSL
  • How to Practice TDD With Kotlin?

Trending

  • Automated Data Extraction Using ChatGPT AI: Benefits, Examples
  • Machine Learning: A Revolutionizing Force in Cybersecurity
  • DZone's Article Types
  • Harnessing the Power of Observability in Kubernetes With OpenTelemetry
  1. DZone
  2. Coding
  3. Languages
  4. DSL Validations: Properties

DSL Validations: Properties

Jakarta Bean Validations are a powerful tool to ensure data quality in your objects, but custom, more complex validations can be difficult to maintain.

By 
Scott Sosna user avatar
Scott Sosna
DZone Core CORE ·
Mar. 28, 24 · Tutorial
Like (1)
Save
Tweet
Share
1.6K Views

Join the DZone community and get the full member experience.

Join For Free

Whether you are aware of it or not, you likely leverage Jakarta Bean Validations to validate class properties or method parameters: @NotBlank, @NotNull and @Email are annotations that inject validation. Furthermore, complete objects and their sub-objects are validated when @Valid is used; e.g., a method parameter void method(@Valid object: Class).

Annotation-based validations on class properties are simple: the validation either passes or fails for that specific property, full stop. More nuanced, complex validations where multiple properties are validated as a unit require implementing a custom class validator, which, in my experience, often devolves into tangled spaghetti code. There must be a better way!

An alternative approach is to create a domain-specific language for class-level validations that is simple to implement, understandable, and extendable, and follows the validation specification.  

Applying the Specification

The specification defines the Validator interface that custom validators implement. The interface defines six possible methods, but the most common (useful) one is:

Kotlin
 
<T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups);


This series of posts will show how to create custom validators for classes using a declarative rather than a coding approach. This post demonstrates implementing individual property validators which are the initial building blocks for the DSL solution.

Implementing Property Validators

Creating a validator for a single property requires extending from a base class and implementing/overriding the method that does the actual validation. Though not required here, the validate method above will be used.

AbstractPropertyValidator

AbstractPropertyValidator is the parent/base class for each validator that implements equals and hashCode for all concrete implementations, useful when validators are held in a collection (e.g., Maps or Sets, which will become more obvious in future posts).

The class has two properties/members provided by the child class during construction:

  • propertyName is informational only, used when creating a violation when validation fails.
  • getter is the getter function for the specific property that is validated from an object. The generic <S> is the validated object's class and <T> is the return data type for the getter.
Kotlin
 
abstract class AbstractPropertyValidator<T,S> (
    protected val propertyName: String,
    protected val getter: S.() -> T?) : AbstractValidator<S>() {

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as AbstractPropertyValidator<*, *>

        return Objects.equals(propertyName, other.propertyName)
            && Objects.equals(getter, other.getter);
    }

    override fun hashCode(): Int {
        return Objects.hash(propertyName, getter)
    }
}


  • NOTE: This abstract class extends AbstractValidator, which is defined later in this post.

Implementing a Validator

For this example, we need to validate that a property's value is either negative and divisible by 3 or positive and divisible by 7; any other values, including null and zero, are invalid.

[Obviously very contrived —  I've never had this requirement in real life, but makes for a fairly straightforward example.]

Let's implement LongSevensAndThreesValidator to do the validation described.  Each property validator class extends AbstractPropertyValidator and implements validate, which:

  • Invokes the getter method to retrieve the property value
  • Validates whether the value is valid or not

 The generic <S> is the class being validated, as described above in AbstractPropertyValidator.  The <T> generic supplied to AbstractPropertyValidator is hard-coded as Long because concrete implementation expects a Long.

Kotlin
 
class LongSevensAndThreesValidator<S> (propertyName: String,
                                       getter: S.() -> Long?)
    : AbstractPropertyValidator<Long, S>(propertyName, getter) {
    override fun validate(source: S,
                          errors: MutableSet<ConstraintViolation<S>>) : Boolean {
        val value = getter.invoke(source)
        return if (value != null &&
            ((value > 0L && value % 7 == 0L) || 
             (value < 0L && value % 3 == 0L))) {
            true
        } else {
            errors.add(createViolation(
                source,
                ERROR_MESSAGE.format(propertyName),
                ERROR_MESSAGE,
                propertyName,
                value))
            false
        }
    }

    companion object {
        private const val ERROR_MESSAGE = "%s must either greater than zero and divisible by 7 or less than zero and divisible by 3"
    }
}


The method createViolation is called when the validation fails and is declared in AbstractValidator (below).

CAVEAT EMPTOR: Execution time increases with complexity, and validating against remote resources — services, databases, files, etc. — increases latency, perhaps dramatically, and introduces more error conditions to handle.

Putting It All Together

In this example, the class MyExampleClass has multiple properties, one of which myValidatedProperty is a Long which is validated by `LongSevensAndThreesValidator. The following is an example code to determine whether the property value is correct or not.

Kotlin
 
val myExampleObject = MyExampleClass(....)

// Create the property validator by specifying which property to check.
val validator = LongSevensAndThreesValidator(
     "myValidatedProperty", 
     MyExampleClass::myValidatedProperty)

// Validate the property
val violations = mutableSetOf<ConstraintViolation<T>>()
validator.validate(myExampleObject, violations)

// empty collection means successful validation
val successfullyValidated = violations.isEmpty()


The Set<ConstraintViolation<T>> contains details about failed validations.  Though a collection is not necessary for a single property, you'll see its usefulness in future posts.

Final Comments

This post introduces property validators as the basic building block upon which the entire DSL is built. Future posts use and extend property validators to implement complete object validations.

Next up is Part 2: DSL Validations: Child Properties.

Supporting Code

PropertyOperatorValidator

This is the base interface that declares the method implemented by all validators. The generic <S> is the class or interface against which a validation is done.

Kotlin
 
interface PropertyOperatorValidator<S> {
    fun validate(source: S,
                 errors: MutableSet<ConstraintViolation<S>>): Boolean
}


AbstractValidator

This base class implements helper methods for creating and collecting violations for failed validations. The generic <S> is the class or interface against which a validation is done.

Kotlin
 
abstract class AbstractValidator<S> (): PropertyOperatorValidator<S> {

    protected fun addViolation(source: S,
                               message: String,
                               messageTemplate: String,
                               property: String,
                               value: Any?,
                               errors: MutableSet<ConstraintViolation<S>>) {
        errors.add(
            createViolation(source,
                message,
                messageTemplate,
                property,
                value))
    }

    protected fun createViolation(source: S,
                                  message: String,
                                  messageTemplate: String,
                                  property: String, value: Any?) : ConstraintViolation<S> {
        return ConcreteConstraintViolationImpl<S>(
            source, 
            message, 
            messageTemplate, 
            property, 
            value)
    }
}


ConcreteConstraintViolation

All  validate methods are passed Set<ConstraintViolation> to which information about failed validations is added, an empty collection indicating all validations succeeded. ConstraintViolation is an interface, so ConcreteConstraintViolation is the actual instance created when the validation fails. The generic <S> is the class or interface against which a validation is done.

Kotlin
 
class DatasiteConstraintViolationImpl<S> (
    private val rootBean: S,
    private val message: String,
    private val messageTemplate: String,
    private val propertyPath: Path,
    private val invalidValue: Any?): ConstraintViolation<S> {

    constructor(
        rootBean: S,
        message: String,
        messageTemplate: String,
        propertyName: String,
        invalidValue: Any?
    )
        : this(
            rootBean,
            message,
            messageTemplate,
            PathImpl.createPathFromString(propertyName),
            invalidValue)

// Define the getters for the ConstraintViolation interface
}


Note: This is post 1 of a 4-part series. Part 2 is DSL Validation: Child Properties

Kotlin (programming language) Data Types Domain-Specific Language Bean Validation

Published at DZone with permission of Scott Sosna. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • New ORM Framework for Kotlin
  • Testcontainers With Kotlin and Spring Data R2DBC
  • Automated CI/CD of Multiple Projects Using TeamCity’s Kotlin DSL
  • How to Practice TDD With Kotlin?

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: