{"id":27219990,"url":"https://github.com/lfr/fsharp.domain.validation","last_synced_at":"2025-04-10T06:50:45.807Z","repository":{"id":41584227,"uuid":"240265681","full_name":"lfr/FSharp.Domain.Validation","owner":"lfr","description":"Designing with types requires a lot of code - this library fixes that","archived":false,"fork":false,"pushed_at":"2024-06-25T18:43:03.000Z","size":11017,"stargazers_count":141,"open_issues_count":23,"forks_count":6,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-04-02T16:51:51.241Z","etag":null,"topics":["ddd","dotnet","fsharp","tiny"],"latest_commit_sha":null,"homepage":"","language":"F#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lfr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"lfr","patreon":null,"open_collective":null,"ko_fi":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"custom":null}},"created_at":"2020-02-13T13:23:04.000Z","updated_at":"2025-03-13T13:12:18.000Z","dependencies_parsed_at":"2024-06-25T20:23:27.958Z","dependency_job_id":"c2d7926e-a91a-4443-969f-588773c95378","html_url":"https://github.com/lfr/FSharp.Domain.Validation","commit_stats":null,"previous_names":["lfr/fsharp.validationblocks"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfr%2FFSharp.Domain.Validation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfr%2FFSharp.Domain.Validation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfr%2FFSharp.Domain.Validation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lfr%2FFSharp.Domain.Validation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lfr","download_url":"https://codeload.github.com/lfr/FSharp.Domain.Validation/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248173852,"owners_count":21059595,"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":["ddd","dotnet","fsharp","tiny"],"created_at":"2025-04-10T06:50:45.000Z","updated_at":"2025-04-10T06:50:45.796Z","avatar_url":"https://github.com/lfr.png","language":"F#","funding_links":["https://github.com/sponsors/lfr"],"categories":[],"sub_categories":[],"readme":"[![nuget](https://img.shields.io/nuget/v/FSharp.Domain.Validation.svg?style=badge\u0026logo=nuget\u0026color=brightgreen\u0026cacheSeconds=21600\u0026label=Standard)](https://www.nuget.org/packages/FSharp.Domain.Validation/)\r\n[![nuget](https://img.shields.io/nuget/v/FSharp.Domain.Validation.Fable.svg?style=badge\u0026logo=nuget\u0026color=brightgreen\u0026cacheSeconds=21600\u0026label=Fable)](https://www.nuget.org/packages/FSharp.Domain.Validation.Fable/)\r\n\u003cbr\u003e\r\n\r\n\u003cp\u003e\r\n    \u003cimg width=\"100%\" src=\"https://raw.githubusercontent.com/lfr/FSharp.Domain.Validation/master/logo/hd.png\"\u003e\r\n\u003c/p\u003e\r\n\r\nA tiny F# library with huge potential to simplify your domain design, as you can see from the examples below:\r\n\u003ca name=\"anchor\"\u003e\r\n| \u003ccenter\u003eWithout this package 👎\u003c/center\u003e | \u003ccenter\u003eUsing this package 👍\u003ca name=\"anchor2\" /\u003e\u003c/center\u003e |\r\n|---|---|\r\n|\u003cpre\u003e\u003ca href=\"#anchor\"\u003e\u003cimg src=\"assets/style-single-case.svg\" alt=\"// Single-case union style\" width=\"100%\" /\u003e\u003c/a\u003e\u003cbr\u003etype Tweet = private Tweet of string\u003cbr\u003emodule Tweet =\u003cbr\u003e  let validate = function\u003cbr\u003e  \u0026#124; s when String.IsNullOrWhitespace s →\u003cbr\u003e     IsMissingOrBlank \u0026#124;\u0026gt; Error\u003cbr\u003e  \u0026#124; s when s.Length \u003e 280 →\u003cbr\u003e     IsTooLong 280 \u0026#124;\u0026gt; Error\u003cbr\u003e  \u0026#124; s → Tweet s \u0026#124;\u0026gt; Ok\u003cbr\u003e  let value (Tweet s) = x in s\u003cbr\u003e\u003cbr\u003e\u003ca href=\"#anchor\"\u003e\u003cimg src=\"assets/style-oo.svg\" alt=\"// Object-oriented style\" width=\"100%\" /\u003e\u003c/a\u003e\u003cbr\u003etype Tweet private (s) = class end with\u003cbr\u003e   static member Validate = function\u003cbr\u003e   \u0026#124; s when String.IsNullOrWhitespace s →\u003cbr\u003e      IsMissingOrBlank \u0026#124;\u0026gt; Error\u003cbr\u003e   \u0026#124; s when s.Length \u003e 280 →\u003cbr\u003e      IsTooLong 280 \u0026#124;\u0026gt; Error\u003cbr\u003e   \u0026#124; s → Tweet s \u0026#124;\u0026gt; Ok\u003cbr\u003e   interface IConstrained\u0026lt;string\u0026gt; with\u003cbr\u003e      member x.Value = s\u003c/pre\u003e\u003ca href=\"#anchor2\"\u003e\u003cimg src=\"assets/scroll.svg\" width=\"80%\" /\u003e\u003c/a\u003e|\u003cpre\u003etype Tweet = private Tweet of Text with\u003cbr\u003e   interface TextBox with\u003cbr\u003e      member _.Validate =\u003cbr\u003e         fun s -\u003e s.Length \u003e 280 =\u003e IsTooLong 280\u003c/pre\u003e\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;\u003csmall\u003e➡\u0026nbsp;\u003c/small\u003e[See the live demo](https://impure.fun/FSharp.Domain.Validation/demo/)\r\n\r\nYou may have noticed that the examples on the left have an additional *not null or empty* validation case. On the right this validation is implicit in the statement that a `Tweet` is a `Tweet of Text`. Since Validation boxes can have inner boxes, the only rules that need to be explicitly declared are the rules specific to the type being defined!\r\n\r\n\r\n\r\n\r\n## Interface? Really?\r\n\r\nF# is a multi-paradigm language, so there's nothing preventing us from harnessing (hijacking?) OP concepts for their expressiveness without any of the baggage. For instance here we use `interface` as an elegant way to both:\r\n\r\n* Identify a type as a Validation box\r\n* Enforce the definition of validation rules\r\n\r\nThere's no other mentions of interfaces in the code that creates or uses Validation boxes, only when defining new types.\r\n\r\n## How it works\r\n\r\nFirst you declare your error types, then you declare your actual domain types (i.e. `Tweet`), and finally you use them with the provided `Box.value` and `Box.validate` functions. These 3 simple steps are enough to ensure at compilation time that your entire domain is **always** valid!\r\n\r\n\u003cp align=\"center\"\u003e\r\n    \u003ca href=\"https://impure.fun/FSharp.Domain.Validation/demo/\"\u003e\r\n        \u003cimg src=\"assets/demo.gif\" alt=\"demo\" /\u003e\r\n    \u003c/a\u003e\r\n    \u003csmall\u003e\u003cbr\u003eOlder version of the live demo for future DDD paleontologists\u003c/small\u003e\r\n\u003c/p\u003e\r\n\r\n### Declaring your errors\r\n\r\nBefore declaring types like the one above, you do need define your error type. This can be a brand new validation-specific discriminated union or part of an existing one.\r\n\r\n```fsharp\r\n// These are just an example, create whatever errors\r\n// you need to return from your own validation rules\r\ntype TextError =\r\n    | ContainsControlCharacters\r\n    | ContainsTabs\r\n    | IsTooLong of int\r\n    | IsMissingOrBlank\r\n    // ...\r\n```\r\n\r\nWhile not strictly necessary, the next single line of code greatly improves the readability of your type declarations by abbreviating the `IBox\u003c_,_\u003e` interface for a specific primitive type.\r\n\r\n```fsharp\r\n// all string-based types can now interface TextBox instead of IBox\u003cstring, TextError\u003e\r\ntype TextBox = inherit IBox\u003cstring, TextError\u003e\r\n```\r\n\r\n### Declaring your types\r\nType declaration is reduced to the absolute minimum. A type is given a name, a private constructor, and the interface above that essentially makes it a **Validation box** and ensures that you define the validation rule.\r\n\r\nThe  validation rule is a function of the primitive type (`string` here) that returns a list of one or more errors depending on the stated conditions.\r\n\r\n```fsharp\r\n/// Single or multi-line non-null non-blank text without any additional validation\r\ntype FreeText = private FreeText of string with\r\n    interface TextBox with\r\n        member _.Validate =\r\n            // validation ruleᵴ (only one)\r\n            fun s -\u003e\r\n                [if s |\u003e String.IsNullOrWhiteSpace then IsMissingOrBlank]\r\n```\r\n\r\n### Simpler validation rules with validation operators\r\nThe type declaration above can be simplified further using the provided `=\u003e` and `==\u003e` operators that here combine a predicate of `string` with the appropriate error.\r\n\r\n```fsharp\r\n/// Alternative type declaration using the ==\u003e operator\r\ntype FreeText = private FreeText of string with\r\n    interface TextBox with\r\n        member _.Validate =\r\n            // same validation rule using validation operators\r\n            String.IsNullOrWhiteSpace ==\u003e IsMissingOrBlank\r\n```\r\nTo use validation operators make sure to open `FSharp.Domain.Validation.Operators` in the file(s) where you declare your Validation types. See [Text.fs](/src/FSharp.Domain.Validation/Example/Text.fs) for more examples of validation operators.\r\n\r\n### Creating and using boxes in your code\r\n\r\nUsing Validation boxes is easy, let's say you have a box called `email`, you can simply access its value using the following:\r\n\r\n```fsharp\r\n// get the primitive value from the box\r\nBox.value email // → string\r\n```\r\n\r\nThere's also an experimental operator `%` that essentially does the same thing. Note that this operator is *opened* automatically along with the namespace `FSharp.Domain.Validation`. To avoid operator pollution this is advertised as experimental until the final operator characters are decided.\r\n\r\n```fsharp\r\n// experimental — same as Box.value\r\n%email // → string\r\n```\r\n\r\nCreating a box is just as simple:\r\n\r\n```fsharp\r\n// create a box, canonicalizing (i.e. trimming) the input if it's a string\r\nBox.validate s // → Ok 'box | Error e\r\n```\r\n\r\n`Box.validate` canonicalization consists of trimming both whitespace and control characters, as well as removing occurrences of the null character. While this should be the preferred way of creating boxes, it's possible to skip canonicalization by using `Box.verbatim` instead.\r\n\r\nWhen type inference isn't possible, specify the box type using the generic parameter:\r\n\r\n```fsharp\r\n// create a box when its type can't be inferred\r\nBox.validate\u003cTweet\u003e s // → Ok Tweet | Error e\r\n```\r\n\r\n⚠ Do **not** force type inference using type annotations as it's unnecessarily verbose:\r\n\r\n```fsharp\r\n// incorrect example, do *not* copy/paste\r\nlet result : Result\u003cEmail, TextError list\u003e = // :(\r\n    Box.validate \"incorrect@dont.do\"\r\n\r\n// correct alternative when type inference isn't available\r\nlet result =\r\n    Box.validate\u003cEmail\u003e \"dev@fsharp.lang\"    // :)\r\n```\r\n\r\nIn both cases `result` is of type `Result\u003cEmail, TextError list\u003e`.\r\n\r\n## Exceptions instead of Error\r\nThe `Box.validate` method returns a `Result`, which may not always be necessary, for instance when de-serializing values that are guaranteed to be valid, you can just use:\r\n\r\n```fsharp\r\n// throws an exception if not valid\r\nUnchecked.boxof \"this better be valid\"         // → 'box (inferred)\r\n\r\n// same as above, when type inference is not available\r\nUnchecked.boxof\u003cText\u003e \"this better be valid 2\" // → Text\r\n```\r\n\r\n## Serialization\r\n\r\nThere's a `System.Text.Json.Serialization.JsonConverter` included, if you add it to your serialization options all boxes are serialized to (and de-serialized from) their primitive type. It is good practice to keep your serialized content independent from implementation considerations such as Validation boxes.\r\n\r\n## Not just strings\r\n\r\nStrings are the perfect example as it's usually the first type for which developers stitch together validation logic, but this library works with anything, you can create a `PositiveInt` that's guaranteed to be greater than zero, or a `FutureDate` that's guaranteed to not be in the past. Lists, vectors, any type of object really, if you can write a predicate against it, you can validate it. It's 100% generic so the sky is the limit.\r\n\r\n## Ok looks good, but I'm still not sure\r\n\r\nI've created a checklist to help you decide whether this library is a good match for your project:\r\n\r\n- [x] My project contains domain objects/records\r\n\r\nIf your project satisfies all of the above this library is for you!\r\n\r\nIt dramatically reduces the amount of code necessary to make illegal states unrepresentable while being tiny and built only with `FSharp.Core`. It uses F# concepts in the way they're meant to be used, so if one day you decide to no longer use it, you can simply get rid of it and still keep all the single-case unions that you've defined. All you'll need to do is create your own implementation of `Box.validate` and `Box.value` or just make the single case constructors public.\r\n\r\n## Ready to try it?\r\n\r\nThere are two packages, make sure you only reference the one you need:\r\n\r\n| Project type | \u003ccenter\u003ePackage\u003c/center\u003e |\r\n|---|:--|\r\n|Standard|[![nuget](https://img.shields.io/nuget/v/FSharp.Domain.Validation.svg?style=badge\u0026logo=nuget\u0026color=brightgreen\u0026cacheSeconds=21600\u0026label=FSharp.Domain.Validation)](https://www.nuget.org/packages/FSharp.Domain.Validation/)|\r\n|Fable|[![nuget](https://img.shields.io/nuget/v/FSharp.Domain.Validation.Fable.svg?style=badge\u0026logo=nuget\u0026color=brightgreen\u0026cacheSeconds=21600\u0026label=FSharp.Domain.Validation.Fable)](https://www.nuget.org/packages/FSharp.Domain.Validation.Fable/)|\r\n\r\nYou can check the [project source code](https://github.com/lfr/FSharp.Domain.Validation/tree/master/src/demo) behind the live demo. You can also look into [Text.fs](/src/FSharp.Domain.Validation/Example/Text.fs) for an example of string boxes which are the by far the most common type of boxes.\r\n\r\n## Conclusion\r\n\r\nUsing this library you can create airtight domain objects guaranteed to never have invalid content. Not only you're writing less code, but your domain definition files are much smaller and nicer to work with. You'll also get [ROP](https://fsharpforfunandprofit.com/rop/) almost for free, and while there is a case to be made [against ROP](https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/), it's definitely a perfect match for content validation, especially content that may be entered by a user.\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flfr%2Ffsharp.domain.validation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flfr%2Ffsharp.domain.validation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flfr%2Ffsharp.domain.validation/lists"}