{"id":15111265,"url":"https://github.com/miquido/validoctor","last_synced_at":"2025-04-12T02:24:58.085Z","repository":{"id":40643656,"uuid":"104922962","full_name":"miquido/validoctor","owner":"miquido","description":"Validoctor is an all-purpose, rule-based input data validator for backend java and kotlin projects. The project was made by Miquido. https://www.miquido.com/","archived":false,"fork":false,"pushed_at":"2023-06-05T11:01:56.000Z","size":338,"stargazers_count":2,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"develop","last_synced_at":"2025-04-12T02:24:49.495Z","etag":null,"topics":["backend","java","kotlin","validation","validation-library","validator"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/miquido.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-09-26T18:29:01.000Z","updated_at":"2023-06-05T11:01:31.000Z","dependencies_parsed_at":"2023-01-30T06:46:26.547Z","dependency_job_id":null,"html_url":"https://github.com/miquido/validoctor","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miquido%2Fvalidoctor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miquido%2Fvalidoctor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miquido%2Fvalidoctor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/miquido%2Fvalidoctor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/miquido","download_url":"https://codeload.github.com/miquido/validoctor/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248506316,"owners_count":21115416,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backend","java","kotlin","validation","validation-library","validator"],"created_at":"2024-09-26T00:02:53.733Z","updated_at":"2025-04-12T02:24:58.065Z","avatar_url":"https://github.com/miquido.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Tests](https://github.com/miquido/validoctor/workflows/Tests/badge.svg?branch=develop)\n\nValidoctor is an all-purpose data validator for backend java and kotlin projects. It performs validation basing on rules \npassed along with object to be validated. Specially designed for simple validation of complex structures.\n\n# Motivation\nValidoctor is a tool that allows defining and performing validations in a clean, simple, deterministic, zero-magic way. \nKey features:\n* validation rules clearly defined and well separated from model objects themselves, imposing no restrictions upon their \nstructure and allowing you to keep them clean\n* no generated code nor annotation processing involved for simple, understandable and readable validation process\n* concise API and rich collection of predefined rules make even the most complex validations require little code\n* flexible, configurable and extensible - its quick and easy to define custom validations and alter Validoctor's behavior\n* readable and useful error reports, ready to be propagated back to clients as they are\n* full control over validation process - call it when you want, run it on whatever number of threads, \nsplit it into several steps if you feel like it, process results however you want\n\n# Getting started\nTo add validoctor to your project:\n```groovy\nrepositories {\n    mavenCentral()\n}\ndependencies {\n  implementation \"com.miquido:validoctor:2.1.2\"\n}\n```\nValidoctor is a data validation library handling validations on complex data structures. It is designed to be \nused as a standalone complex validation solution, and not for cooperation with Bean Validation or any other \nvalidation solution.\n\nBasic concepts and a production-grade example are shown below. For more specific use cases, please refer to tests. \nWhen in doubt about how something should be used, answer is usually easily found there.\n\n# Vocabulary\n* Ailment - like a single violation of a Rule. It carries a violator's name (typically a field name) and a violation message.\n* Diagnosis - validation report stating whether object is valid or not and containing all Ailments discovered in it. \nIt is structured to be useful for any client code that may want to read it, so it may be used directly as, for example, \na response body.\n\n# Quick start examples\nMain class to use is Validoctor. All its methods are static. You can set global behavior in cases of failed validations \nusing `setThrowing(boolean)` and `setExceptionFactory(Function\u003cDiagnosis, RuntimeException\u003e)` methods.\n```java\nValidoctor.setThrowing(false); //will return Diagnosis objects after validating\nValidoctor.setThrowing(true): //will throw DiagnosisExceptions on failed validations\nValidoctor.setExceptionFactory(diagnosis -\u003e new CustomException(diagnosis)); //will throw CustomExceptions instead, if setThrowing is true\n```\nFor validating primitive or String values, just use `SimpleRule`s. Most commonly used ones are supplied in Rules class.\n```java\nDiagnosis diagnosis = Validoctor.examine(stringToValidate, notNull(), stringAlphabetic(), stringTrimmedNotEmpty());\n```\nThis will check if `stringToValidate` is not null, not empty or whitespace only and only containing letters.\n\nValidoctor's main strength lies in validating complex objects in one call. For this, a special `RuleBuilder` is used. \nFor example this validates fields foo and bar of object `Example` with specified rules:\n```java\nRule\u003cExample\u003e exampleRule = Validoctor.rulesFor(Example.class)\n        .field(\"foo\", notNull(), numberPositive())\n        .field(\"bar\", stringMinLength(3), stringMaxLength(20))\n        .build();\n```\nIt is then passed for Validoctor's examination the same way as `SimpleRule`s:\n```java\nDiagnosis diagnosis = Validoctor.examine(exampleObject, exampleRule);\n```\nIt can also accept several rules just as it could accept several `SimpleRule`s.\nYou will find that builder returned by `rulesFor` class provides a multitude of other options for attaching rules to fields. \nIt is possible to attach rule sets to multiple fields at once, to all fields of same type, to reductions of two fields,\nto elements of collection that is a field in the object etc. Rules can also be made conditional and dependent on success \nof other rules.\n\nAll Rules are stateless and can be reused for multiple validations.\n\n# Production grade example\nImagine you need to validate an instance of a `Product` defined like this:\n```kotlin\n//Those are Kotlin data classes, for pure Java people out there it is the same as annotating a class with Lombok's @Data\ndata class NutritionFacts(val kcal: Int?, \n                          val fibre: Double?, \n                          val protein: Double?, \n                          val fat: Double?)\n\ndata class Comment(val authorId: Long?, \n                   val text: String?)\n\ndata class Product(val name: String?, \n                   val skuId: String?, \n                   val description: String?, \n                   val weightG: Float?, \n                   val volumeMl: Float?,\n                   val nutritionFacts: NutritionFacts?, \n                   val glutenFree: Boolean?, \n                   val vegan: Boolean?,\n                   val comments: List\u003cComment\u003e?, \n                   val reviewScores: List\u003cInt\u003e?)\n```\n\nLet's start with thinking about what we want to validate. Thinking on a per-class basis is the recommended approach \nfor using Validoctor. So, starting with `NutritionFacts` class, we surely want all the values to be positive. \nWe write a simple rule for that:\n```kotlin\nval nutritionFactsRule =\n  Validoctor.rulesFor(NutritionFacts::class.java)\n    .allAssignable(Number::class.java, numberPositive())\n    .build()\n```\n`Validoctor.rulesFor` defines a list of rules that are applied to specified fields of the validated object. \nValidoctor uses reflection to find the values of the fields to apply the rules to. \nWe used `allAssignable` to tell the rule that we want it to read all fields of type `Number` (or its subtypes) and apply \nthe `numberPositive` rule to each of them. It is a predefined rule available in `Rules` class that just checks if number is \nlarger than 0.\n\nOk, so that is what we want to validate in `NutritionFacts`. Let's move on to `Comment` class. We certainly need a valid, \nnon-null `authorId`, and we want the `text` of the comment to be not empty, not longer that certain characters count and not \ncontain any inappropriate words. For that, we will first need to define our own custom censor rule like this:\n```kotlin\nval stringCensorRule: SimpleRule\u003cString\u003e =\n  SimpleRule(\"CENSORED_WORD\") { str -\u003e str == null || !str.contains(\"fuck\", true) }\n```\nIn case when there is no appropriate predefined rule available in `Rules` class, we can create our own rules as shown above. \nUsing SimpleRule constructor should cover 99.99% cases, so if you find yourself wanting to do something more complicated \nit might be a sign that you are trying to over-engineer. What we did here is we specified a violation message to show when \nrule fails, and a predicate that will be applied to patients to determine whether they are valid or not.\n\nNow, we are ready to create a rule for Comment object that will specify all validations we need:\n```kotlin\nval commentRules =\n  Validoctor.rulesFor(Comment::class.java)\n    .field(\"authorId\", notNull(), numberPositive())\n    .field(\"text\", notNull(), stringTrimmedNotEmpty(), stringMaxLength(500), stringCensorRule)\n    .build()\n```\nWe used `Validoctor.rulesFor` just like for `NutritionFacts`. This time though, we specify fields we want the rules to be \napplied to one by one, by their name. Each `field` call will make resulting rule apply all the specified rules to given field. \nSo here, `authorId` will be checked if it is not null and then if it is a positive number, and `text` will be checked for nullity, \nnot emptiness, max allowed length and inappropriate words with our custom censor rule.\n\nAnd now, for the biggest task: we need to apply a series of various validations to fields of `Product` class. For better \nreadability, we can decide to split validations of such complex classes into a few separate rules. Let's do that here and \nfirst specify rules that deal exclusively with nullity of `Product`'s fields:\n```kotlin\nfun nullityRules(p: Product) =\n  Validoctor.rulesFor(Product::class.java)\n    .allTyped(Boolean::class.java, notNull())\n    .allTyped(Float::class.java, notNull())\n    .field(\"name\", notNull\u003cString\u003e())\n    .field(\"nutritionFacts\", conditional({ p.skuId != null }, notNull\u003cNutritionFacts\u003e()))\n    .field(\"nutritionFacts\", conditional({ p.skuId == null }, isNull\u003cNutritionFacts\u003e()))\n    .build()\n```\nWhat we did here is we required all `Booleans` (`glutenFree` and `vegan` fields) and all `Floats` (`weightG`, `volumeMl`)\nto be not null. `allTyped` used here differs from `allAssignable` we have seen in `nutritionFactsRule` in that it will \nonly match fields strictly of specified types, and not their subtypes.\nNext, we specified that we need `name` field to not be null (`notNull` predefined rule needs a type \nparameter in Kotlin if there are no other rules passed that allow inferring the type of field). \nLast two `field` calls deal with `nutritionFacts` field, and differ from what we have seen so far in that the rules passed  \nare wrapped in `conditional` accepting an additional `Predicate`. Those rules will only be applied if that predicate is \nfulfilled. This allows us to require the nutrition facts are present only if we also have the `skuId` of the product, \nand are null otherwise. This is also the reason why we defined this Rule in a function and not in a field - we read the \n`Product` argument to be able to dynamically determine `nutritionFacts` nullity rules.\n\nNow, we also need to validate a bunch of other stuff on `Product` object. Let's look at the last rule we need:\n```kotlin\nfun validityRules(p: Product) =\n  Validoctor.rulesFor(Product::class.java)\n    .field(\"name\", stringTrimmedNotEmpty(), stringMaxLength(40))\n    .field(\"skuId\", stringExactLength(10), stringAlphanumeric())\n    .field(\"description\", stringMaxLength(200))\n    .allTyped(Float::class.java, numberPositive())\n    .field(\"nutritionFacts\", conditional({ p.nutritionFacts != null }, nutritionFactsRule))\n    .elements(\"comments\", commentRules)\n    .elements(\"reviewScores\", chained(notNull(), numberInRange(1, 5)))\n    .build()\n```\nTop `field` and `allTyped` calls are nothing new at this point. New hot stuff is `nutritionFacts` rule definition. \nIt specifies that `nutritionFacts` field will be validated with the Rule we created for `NutritionFacts` objects at the \nbeginning of this example. You can nest Rules for fields of complex types like this, down to hierarchies of unlimited depth. \nIn this case, validation of the `nutritionFactsRule` is also conditional, only applied if the `nutritionFacts` field is not null. \nAnother important case here is `elements` method we used for `comments` and `reviewScores`. This method allows us to apply \nRules to each element of the collection field instead of field itself. So we attached the Rule we defined above for `Comment` \nto each element of `comments` list. Similarly, we attached a Rule to each element in `reviewScores` collection, but this time, \nwe also used a `chained` wrapper. It accepts any number of Rules that will be executed sequentially - if any of these Rules \nfails, none of the ones coming after it will be executed. `chained` is compatible with `conditional`, and you can mix and \nmatch any and all Rules in one call.\n\nWith that we have defined all the validation we need for given data structure. To perform the validation on an actual \nobject, we just need one call:\n```kotlin\nval diagnosis = Validoctor.examine(product, nullityRules, validityRules)\n```\nAnd that's it. Diagnosis object returned by Validoctor contains the result and all the Ailments found in the object.\n\n# Usage with Spring's @ExceptionHandler\nDiagnosis objects are designed to be easily processable and readable by any client applications they are returned to. \nUsing Validoctor with `setThrowing(true)` you can make it throw `DiagnosisException` on Rule violations. Then, if you are \nusing Spring, you can intercept this exception and easily return `Diagnosis` as response body with whatever status code \nyou desire. Example `ExceptionHandler` could be defined like this:\n\n```kotlin\n@ControllerAdvice\nclass ExceptionHandler: ResponseEntityExceptionHandler() {\n\n  @ExceptionHandler(DiagnosisException::class)\n  fun handleDiagnosisException(ex: DiagnosisException): ResponseEntity\u003cAny\u003e {\n    return ResponseEntity.unprocessableEntity().body(ex.diagnosis)\n  }\n}\n```\n\n\n# Dependencies\nNone.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmiquido%2Fvalidoctor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmiquido%2Fvalidoctor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmiquido%2Fvalidoctor/lists"}