{"id":37051570,"url":"https://github.com/desjoerd/optionalvalues","last_synced_at":"2026-01-14T05:57:34.667Z","repository":{"id":259477804,"uuid":"876572541","full_name":"desjoerd/OptionalValues","owner":"desjoerd","description":"Know whether a property was Specified or Unspecified/omitted in your (json) objects. ","archived":false,"fork":false,"pushed_at":"2025-12-15T12:59:36.000Z","size":223,"stargazers_count":51,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-12-17T06:58:04.311Z","etag":null,"topics":["dotnet","json","json-merge-patch","merge-patch","openapi","patch","systemtextjson"],"latest_commit_sha":null,"homepage":"","language":"C#","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/desjoerd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-10-22T07:44:08.000Z","updated_at":"2025-12-15T12:41:17.000Z","dependencies_parsed_at":"2025-04-02T15:31:04.610Z","dependency_job_id":"0296ea2c-e067-48b1-8cb3-1430ce6c1fbe","html_url":"https://github.com/desjoerd/OptionalValues","commit_stats":null,"previous_names":["desjoerd/optionalvalues"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/desjoerd/OptionalValues","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/desjoerd%2FOptionalValues","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/desjoerd%2FOptionalValues/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/desjoerd%2FOptionalValues/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/desjoerd%2FOptionalValues/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/desjoerd","download_url":"https://codeload.github.com/desjoerd/OptionalValues/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/desjoerd%2FOptionalValues/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28412172,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T05:26:33.345Z","status":"ssl_error","status_checked_at":"2026-01-14T05:21:57.251Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["dotnet","json","json-merge-patch","merge-patch","openapi","patch","systemtextjson"],"created_at":"2026-01-14T05:57:34.223Z","updated_at":"2026-01-14T05:57:34.658Z","avatar_url":"https://github.com/desjoerd.png","language":"C#","readme":"# OptionalValues\n\nA .NET library that provides an `OptionalValue\u003cT\u003e` type, representing a value that may or may not be specified, with comprehensive support for JSON serialization. e.g. (`undefined`, `null`, `\"value\"`)\n\n[![NuGet](https://img.shields.io/nuget/v/OptionalValues.svg)](https://www.nuget.org/packages/OptionalValues)\n[![License](https://img.shields.io/github/license/desjoerd/OptionalValues)](https://github.com/desjoerd/OptionalValues/blob/main/LICENSE)\n![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/desjoerd/OptionalValues/.github%2Fworkflows%2Fci.yml)\n\n\n| Package                                                                                           | Version                                                                                                                                        |\n| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |\n| [OptionalValues](https://www.nuget.org/packages/OptionalValues)                                   | [![NuGet](https://img.shields.io/nuget/v/OptionalValues.svg)](https://www.nuget.org/packages/OptionalValues)                                   |\n| [OptionalValues.OpenApi](https://www.nuget.org/packages/OptionalValues.OpenApi)                   | [![NuGet](https://img.shields.io/nuget/v/OptionalValues.OpenApi.svg)](https://www.nuget.org/packages/OptionalValues.OpenApi)                   |\n| [OptionalValues.Swashbuckle](https://www.nuget.org/packages/OptionalValues.Swashbuckle)           | [![NuGet](https://img.shields.io/nuget/v/OptionalValues.Swashbuckle.svg)](https://www.nuget.org/packages/OptionalValues.Swashbuckle)           |\n| [OptionalValues.NSwag](https://www.nuget.org/packages/OptionalValues.NSwag)                       | [![NuGet](https://img.shields.io/nuget/v/OptionalValues.NSwag.svg)](https://www.nuget.org/packages/OptionalValues.NSwag)                       |\n| [OptionalValues.DataAnnotations](https://www.nuget.org/packages/OptionalValues.DataAnnotations)   | [![NuGet](https://img.shields.io/nuget/v/OptionalValues.DataAnnotations.svg)](https://www.nuget.org/packages/OptionalValues.DataAnnotations)   |\n| [OptionalValues.FluentValidation](https://www.nuget.org/packages/OptionalValues.FluentValidation) | [![NuGet](https://img.shields.io/nuget/v/OptionalValues.FluentValidation.svg)](https://www.nuget.org/packages/OptionalValues.FluentValidation) |\n\n## Overview\n\nThe `OptionalValue\u003cT\u003e` struct is designed to represent a value that can be in one of three states:\n\n- **Unspecified**: The value has not been specified. (e.g. `undefined`)\n- **Specified with a non-null value**: The value has been specified and is `not null`.\n- **Specified with a `null` value**: The value has been specified and is `null`.\n\n### Why\nWhen working with Json it's currently difficult to know whether a property was omitted or explicitly `null`. This makes it hard to support older clients that don't send all properties in a request. By using `OptionalValue\u003cT\u003e` you can distinguish between `null` and `Unspecified` values.\n\n```csharp\nusing System.Text.Json;\nusing OptionalValues;\n\nvar jsonSerializerOptions = new JsonSerializerOptions()\n    .AddOptionalValueSupport();\n\nvar json =\n    \"\"\"\n    {\n      \"FirstName\": \"John\",\n      \"LastName\": null\n    }\n    \"\"\";\n\nvar person1 = JsonSerializer.Deserialize\u003cPerson\u003e(json, jsonSerializerOptions);\n\n// equals:\nvar person2 = new Person\n{\n    FirstName = \"John\",\n    LastName = null,\n    Address = OptionalValue\u003cstring\u003e.Unspecified // or default\n};\n\nbool areEqual = person1 == person2; // True\n\nstring serialized = JsonSerializer.Serialize(person2, jsonSerializerOptions);\n// Output: {\"FirstName\":\"John\",\"LastName\":null}\n\npublic record Person\n{\n    public OptionalValue\u003cstring\u003e FirstName { get; set; }\n    public OptionalValue\u003cstring?\u003e LastName { get; set; }\n    public OptionalValue\u003cstring\u003e Address { get; set; }\n}\n```\n\n## Installation\n\nInstall the package using the .NET CLI:\n\n```bash\ndotnet add package OptionalValues\n```\n\nFor JSON serialization support, configure the `JsonSerializerOptions` to include the `OptionalValue\u003cT\u003e` converter:\n\n```csharp\nvar options = new JsonSerializerOptions()\n    .AddOptionalValueSupport();\n```\n\nOptionally, install one or more extension packages:\n\n```bash\ndotnet add package OptionalValues.Swashbuckle\ndotnet add package OptionalValues.NSwag\ndotnet add package OptionalValues.DataAnnotations\ndotnet add package OptionalValues.FluentValidation\n```\n\n## Features\n\n- **Distinguish Between Unspecified and Null Values**: Clearly differentiate when a value is intentionally `null` versus when it has not been specified at all. This allows for mapping `undefined` values in JSON to `Unspecified` values in C#.\n- **JSON Serialization Support**: Includes a custom JSON converter and TypeResolverModifier that correctly handles serialization and deserialization, ensuring unspecified values are omitted from JSON outputs.\n- **Dictionary Extensions**: Extension methods for working with dictionaries and `OptionalValue\u003cT\u003e`, including `GetOptionalValue`, `AddOptionalValue`, `TryAddOptionalValue`, and `SetOptionalValue`.\n- **Optional DataAnnotations**: An extension library that provides support for DataAnnotations validation attributes on `OptionalValue\u003cT\u003e` properties.\n- **FluentValidation Extensions**: Provides extension methods to simplify the validation of `OptionalValue\u003cT\u003e` properties using FluentValidation.\n- **OpenApi/Swagger Support**: \n  - **ASP.NET Core OpenAPI**: Support for ASP.NET Core's built-in OpenAPI support (Microsoft.AspNetCore.OpenApi) is available through the `OptionalValues.OpenApi` package. It provides a schema transformer to correctly handle `OptionalValue\u003cT\u003e` types.\n  - **Swashbuckle** Includes a custom data contract resolver for Swashbuckle to generate accurate OpenAPI/Swagger documentation.\n  - **NSwag**: Support for NSwag is available through the `OptionalValues.NSwag` package. It includes an `OptionalValueTypeMapper` to map the `OptionalValue\u003cT\u003e` to its underlying type `T` in the generated OpenAPI schema.\n- **Patch Operation Support**: Ideal for API patch operations where fields can be updated to `null` or remain unchanged.\n\n# Table of Contents\n\n- [OptionalValues](#optionalvalues)\n  - [Overview](#overview)\n    - [Why](#why)\n  - [Installation](#installation)\n  - [Features](#features)\n- [Table of Contents](#table-of-contents)\n- [Usage](#usage)\n  - [Creating an OptionalValue](#creating-an-optionalvalue)\n  - [Checking If a Value Is Specified](#checking-if-a-value-is-specified)\n  - [Accessing the Value](#accessing-the-value)\n  - [Implicit Conversions](#implicit-conversions)\n  - [Equality Comparisons](#equality-comparisons)\n  - [Dictionary Extensions](#dictionary-extensions)\n  - [JSON Serialization with System.Text.Json](#json-serialization-with-systemtextjson)\n    - [Serialization Behavior](#serialization-behavior)\n    - [Deserialization Behavior](#deserialization-behavior)\n    - [Respect nullable annotations](#respect-nullable-annotations)\n- [Library support](#library-support)\n  - [ASP.NET Core](#aspnet-core)\n  - [ASP.NET Core OpenAPI](#aspnet-core-openapi)\n    - [Installation](#installation-1)\n  - [Swashbuckle](#swashbuckle)\n    - [Installation](#installation-2)\n  - [NSwag](#nswag)\n    - [Installation](#installation-3)\n  - [System.ComponentModel.DataAnnotations](#systemcomponentmodeldataannotations)\n  - [FluentValidation](#fluentvalidation)\n    - [Installation](#installation-4)\n    - [Using OptionalRuleFor](#using-optionalrulefor)\n    - [How It Works](#how-it-works)\n    - [Example Usage](#example-usage)\n- [Use Cases](#use-cases)\n  - [API Patch Operations](#api-patch-operations)\n- [Current Limitations](#current-limitations)\n- [Contributing](#contributing)\n- [License](#license)\n- [Benchmarks](#benchmarks)\n\n\n# Usage\n\n## Creating an OptionalValue\n\nYou can create an `OptionalValue\u003cT\u003e` in several ways:\n\n- **Unspecified Value**:\n\n  ```csharp\n  var unspecified = new OptionalValue\u003cstring\u003e();\n  // or\n  var unspecified = OptionalValue\u003cstring\u003e.Unspecified;\n  // or\n  OptionalValue\u003cstring\u003e unspecified = default;\n  ```\n\n- **Specified Value**:\n\n  ```csharp\n  var specifiedValue = new OptionalValue\u003cstring\u003e(\"Hello, World!\");\n  // or using implicit conversion\n  OptionalValue\u003cstring\u003e specifiedValue = \"Hello, World!\";\n  ```\n\n- **Specified Null Value**:\n\n  ```csharp\n  var specifiedNull = new OptionalValue\u003cstring?\u003e(null);\n  // or using implicit conversion\n  OptionalValue\u003cstring?\u003e specifiedNull = null;\n  ```\n\n## Checking If a Value Is Specified\n\nUse the `IsSpecified` property to determine if the value has been specified:\n\n```csharp\nif (optionalValue.IsSpecified)\n{\n    Console.WriteLine(\"Value is specified.\");\n}\nelse\n{\n    Console.WriteLine(\"Value is unspecified.\");\n}\n```\n\n## Accessing the Value\n\n- `.Value`: Gets the value if specified; returns `null` if unspecified.\n- `.SpecifiedValue`: Gets the specified value; throws `InvalidOperationException` if the value is unspecified.\n- `.GetSpecifiedValueOrDefault()`: Gets the specified value or the default value of `T` if unspecified.\n- `.GetSpecifiedValueOrDefault(T defaultValue)`: Gets the specified value or the provided default value if unspecified.\n\n```csharp\nvar optionalValue = new OptionalValue\u003cstring\u003e(\"Example\");\n\n// Using Value\nstring? value = optionalValue.Value;\n\n// Using SpecifiedValue\nstring specifiedValue = optionalValue.SpecifiedValue;\n\n// Using GetSpecifiedValueOrDefault\nstring valueOrDefault = optionalValue.GetSpecifiedValueOrDefault(\"Default Value\");\n```\n\n## Implicit Conversions\n\n`OptionalValue\u003cT\u003e` supports implicit conversions to and from `T`:\n\n```csharp\n// From T to OptionalValue\u003cT\u003e\nOptionalValue\u003cint\u003e optionalInt = 42;\n\n// From OptionalValue\u003cT\u003e to T (returns null if unspecified)\nint? value = optionalInt;\n```\n\n## Equality Comparisons\n\nEquality checks consider both the `IsSpecified` property and the `Value`:\n\n```csharp\nvar value1 = new OptionalValue\u003cstring\u003e(\"Test\");\nvar value2 = new OptionalValue\u003cstring\u003e(\"Test\");\nvar unspecified = new OptionalValue\u003cstring\u003e();\n\nbool areEqual = value1 == value2; // True\nbool areUnspecifiedEqual = unspecified == new OptionalValue\u003cstring\u003e(); // True\n```\n\n## Dictionary Extensions\n\nExtension methods for working with dictionaries and `OptionalValue\u003cT\u003e`:\n\n```csharp\nusing OptionalValues.Extensions;\n\nvar settings = new Dictionary\u003cstring, int\u003e { [\"timeout\"] = 30 };\n\n// Get value as OptionalValue (returns Unspecified if key not found)\nOptionalValue\u003cint\u003e timeout = settings.GetOptionalValue(\"timeout\"); // IsSpecified == true\nOptionalValue\u003cint\u003e retries = settings.GetOptionalValue(\"retries\"); // IsSpecified == false\n\n// Add/Set only when value is specified\nsettings.AddOptionalValue(\"maxRetries\", new OptionalValue\u003cint\u003e(3)); // Adds the value\nsettings.AddOptionalValue(\"other\", OptionalValue\u003cint\u003e.Unspecified); // Does nothing\nsettings.SetOptionalValue(\"timeout\", new OptionalValue\u003cint\u003e(60));   // Updates to 60\n```\n\n## JSON Serialization with System.Text.Json\n\n`OptionalValue\u003cT\u003e` includes a custom JSON converter and JsonTypeInfoResolver Modifier to handle serialization and deserialization of optional values.\nTo properly serialize `OptionalValue\u003cT\u003e` properties, add it to the `JsonSerializerOptions`:\n\n```csharp\nvar newOptionsWithSupport = JsonSerializerOptions.Default\n    .WithOptionalValueSupport();\n\n// or\nvar options = new JsonSerializerOptions();\noptions.AddOptionalValueSupport();\n```\n\n### Serialization Behavior\n\n- **Unspecified Values**: Omitted from the JSON output.\n- **Specified Null Values**: Serialized with a `null` value.\n- **Specified Non-Null Values**: Serialized with the actual value.\n\n```csharp\npublic class Person\n{\n    public OptionalValue\u003cstring\u003e FirstName { get; set; }\n\n    public OptionalValue\u003cstring\u003e LastName { get; set; }\n}\n\n// Creating a Person instance\nvar person = new Person\n{\n    FirstName = \"John\", // Specified non-null value\n    LastName = new OptionalValue\u003cstring\u003e() // Unspecified\n};\n\n// Serializing to JSON\nstring json = JsonSerializer.Serialize(person);\n// Output: {\"FirstName\":\"John\"}\n```\n\n### Deserialization Behavior\n\n- **Missing Properties**: Deserialized as unspecified values.\n- **Properties with `null`**: Deserialized as specified with a `null` value.\n- **Properties with Values**: Deserialized as specified with the given value.\n\n```csharp\nstring jsonInput = @\"{\"\"FirstName\"\":\"\"John\"\",\"\"LastName\"\":null}\";\nvar person = JsonSerializer.Deserialize\u003cPerson\u003e(jsonInput);\n\nbool isFirstNameSpecified = person.FirstName.IsSpecified; // True\nstring firstName = person.FirstName.SpecifiedValue; // \"John\"\n\nbool isLastNameSpecified = person.LastName.IsSpecified; // True\nstring lastName = person.LastName.SpecifiedValue; // null\n```\n\n### Respect nullable annotations\n\n`OptionalValue\u003cT\u003e` has support for respecting nullable annotations when enabling `RespectNullableAnnotations = true` in the `JsonSerializerOptions`. When enabled, when deserializing a `null` value on an `OptionalValue` which is NOT nullable, it will throw a `JsonException` with a message indicating that the value is not nullable.\n\n```csharp\nJsonSerializerOptions Options = new JsonSerializerOptions\n{\n    RespectNullableAnnotations = true,\n}.AddOptionalValueSupport();\n\nvar json = \"\"\"\n           {\n               \"NotNullable\": null\n           }\n           \"\"\";\n\nvar model = JsonSerializer.Deserialize\u003cModel\u003e(json, Options); // Throws JsonException\n\nprivate class Model\n{\n    public OptionalValue\u003cstring\u003e NotNullable { get; init; }\n}\n```\n\nThere are a few limitations to this feature:\n- It only works when NOT using generics.\n\n```csharp\n// it does not work with this, because the type is generic and we cannot determine if it is nullable or not as this information is not available at runtime.\npublic class Model\u003cT\u003e\n{\n    public OptionalValue\u003cT\u003e NotNullable { get; init; }\n}\n```\n\n# Library support\n\n## ASP.NET Core\n\nThe `OptionalValues` library integrates seamlessly with ASP.NET Core, allowing you to use `OptionalValue\u003cT\u003e` properties in your API models.\n\nYou only need to configure the `JsonSerializerOptions` to include the `OptionalValue\u003cT\u003e` converter:\n\n```csharp\n// For Minimal API\nbuilder.Services.ConfigureHttpJsonOptions(jsonOptions =\u003e\n{\n    // Make sure that AddOptionalValueSupport() is the last call when you are using the `TypeInfoResolverChain` of the `SerializerOptions`.\n    jsonOptions.SerializerOptions.AddOptionalValueSupport();\n});\n\n// For MVC\nbuilder.Services.AddControllers()\n    .AddJsonOptions(options =\u003e\n    {\n        options.JsonSerializerOptions.AddOptionalValueSupport();\n    });\n```\n\n## ASP.NET Core OpenAPI\n\nThe `OptionalValues.OpenApi` package provides support for ASP.NET Core's built-in OpenAPI support (Microsoft.AspNetCore.OpenApi) to generate accurate OpenAPI documentation for `OptionalValue\u003cT\u003e` properties.\n\nIt correctly unwraps the `OptionalValue\u003cT\u003e` type and generates the appropriate schema for the underlying type `T`.\n\n### Installation\n\nInstall the package using the .NET CLI:\n\n```bash\ndotnet add package OptionalValues.OpenApi\n```\n\nConfigure the OpenAPI services to use the `OptionalValue\u003cT\u003e` schema transformer:\n\n```csharp\nbuilder.Services.AddOpenApi(options =\u003e\n{\n    options.AddOptionalValueSupport();\n});\n```\n\n## Swashbuckle\n\nThe `OptionalValues.Swashbuckle` package provides a custom data contract resolver for Swashbuckle to generate accurate OpenAPI/Swagger documentation for `OptionalValue\u003cT\u003e` properties.\n\nIt correctly unwraps the `OptionalValue\u003cT\u003e` type and generates the appropriate schema for the underlying type `T`.\n\n### Installation\n\nInstall the package using the .NET CLI:\n\n```bash\ndotnet add package OptionalValues.Swashbuckle\n```\n\nConfigure the Swashbuckle services to use the `OptionalValueDataContractResolver`:\n\n```csharp\nbuilder.Services.AddSwaggerGen();\n// after AddSwaggerGen when you want it to use an existing custom ISerializerDataContractResolver.\nbuilder.Services.AddSwaggerGenOptionalValueSupport();\n```\n\n## NSwag\n\nThe `OptionalValues.NSwag` package provides an `OptionalValueTypeMapper` to map the `OptionalValue\u003cT\u003e` to its underlying type `T` in the generated OpenAPI schema.\n\n### Installation\n\nInstall the package using the .NET CLI:\n\n```bash\ndotnet add package OptionalValues.NSwag\n```\n\nConfigure the NSwag SchemaSettings to use the `OptionalValueTypeMapper`:\n\n```csharp\nbuilder.Services.AddOpenApiDocument(options =\u003e\n{\n    // Add OptionalValue support to NSwag\n    options.SchemaSettings.AddOptionalValueSupport();\n});\n```\n\n## System.ComponentModel.DataAnnotations\n\nThe `OptionalValues.DataAnnotations` package provides DataAnnotations validation attributes for `OptionalValue\u003cT\u003e` properties. They are all overrides of the standard DataAnnotations attributes and prefixed with `Optional`. The key difference is that the validation rules are only applied when the value is `specified` (which is close to the default behavior which only applies it when it's not null).\n\nInstall the package using the .NET CLI:\n\n```bash\ndotnet add package OptionalValues.DataAnnotations\n```\n\n**Presence Validators:**\n\n- `[Specified]`: Ensures the `OptionalValue\u003cT\u003e` is specified (present), but allows `null` or empty values.\n- `[RequiredValue]`: Ensures the `OptionalValue\u003cT\u003e` is specified and its value is not `null` or empty. This should be used instead of the standard `[Required]` attribute.\n\nExample usage:\n```csharp\npublic class ExampleModel\n{\n    [OptionalAllowedValues(\"a\")]\n    public OptionalValue\u003cstring\u003e AllowedValues { get; set; }\n\n    [OptionalDeniedValues(\"a\")]\n    public OptionalValue\u003cstring\u003e DeniedValues { get; set; }\n\n    [OptionalLength(1, 5)]\n    public OptionalValue\u003cint[]\u003e LengthCollection { get; set; }\n\n    [OptionalLength(1, 5)]\n    public OptionalValue\u003cstring\u003e LengthString { get; set; }\n\n    [OptionalMaxLength(5)]\n    public OptionalValue\u003cint[]\u003e MaxLengthCollection { get; set; }\n\n    [OptionalMaxLength(5)]\n    public OptionalValue\u003cstring\u003e MaxLengthString { get; set; }\n\n    [OptionalMinLength(1)]\n    public OptionalValue\u003cint[]\u003e MinLengthCollection { get; set; }\n\n    [OptionalMinLength(1)]\n    public OptionalValue\u003cstring\u003e MinLengthString { get; set; }\n\n    [OptionalRange(5, 42)]\n    public OptionalValue\u003cint\u003e Range { get; set; }\n\n    [OptionalRegularExpression(\"^something$\")]\n    public OptionalValue\u003cstring\u003e RegularExpression { get; set; }\n\n    [Specified]\n    public OptionalValue\u003cstring?\u003e Specified { get; set; }\n\n    [RequiredValue]\n    public OptionalValue\u003cstring\u003e SpecifiedRequired { get; set; }\n\n    [OptionalStringLength(5)]\n    public OptionalValue\u003cstring\u003e StringLength { get; set; }\n}\n```\n\n## FluentValidation\n\nThe `OptionalValues.FluentValidation` package provides extension methods to simplify the validation of `OptionalValue\u003cT\u003e` properties using FluentValidation.\n\n### Installation\n\nInstall the package using the .NET CLI:\n\n```bash\ndotnet add package OptionalValues.FluentValidation\n```\n\n### Using OptionalRuleFor\n\nThe `OptionalRuleFor` extension method allows you to define validation rules for `OptionalValue\u003cT\u003e` properties that are only applied when the value is specified.\n\n```csharp\nusing FluentValidation;\nusing OptionalValues.FluentValidation;\n\npublic class UpdateUserRequest\n{\n    public OptionalValue\u003cstring?\u003e Email { get; set; }\n    public OptionalValue\u003cint\u003e Age { get; set; }\n}\n\npublic class UpdateUserRequestValidator : AbstractValidator\u003cUpdateUserRequest\u003e\n{\n    public UpdateUserRequestValidator()\n    {\n        this.OptionalRuleFor(x =\u003e x.Email, x =\u003e x\n            .NotEmpty()\n            .EmailAddress());\n\n        this.OptionalRuleFor(x =\u003e x.Age, x =\u003e x\n            .GreaterThan(18));\n    }\n}\n```\n\nIn this example:\n\n- The validation rules for `Email` and `Age` are applied only if the corresponding `OptionalValue\u003cT\u003e` is specified.\n- If the value is unspecified, the validation rules are skipped.\n\n### How It Works\n\nThe `OptionalRuleFor` method:\n\n- Takes an expression specifying the `OptionalValue\u003cT\u003e` property.\n- Accepts a configuration function where you define your validation rules using the standard FluentValidation syntax.\n- Internally, it checks if the value is specified (`IsSpecified`) before applying the validation rules.\n\n### Example Usage\n\n```csharp\nvar validator = new UpdateUserRequestValidator();\n\n// Valid request with specified values\nvar validRequest = new UpdateUserRequest\n{\n    Email = \"user@example.com\",\n    Age = 25\n};\n\nvar result = validator.Validate(validRequest);\n// result.IsValid == true\n\n// Invalid request with specified values\nvar invalidRequest = new UpdateUserRequest\n{\n    Email = \"invalid-email\",\n    Age = 17\n};\n\nvar resultInvalid = validator.Validate(invalidRequest);\n// resultInvalid.IsValid == false\n// Errors for Email and Age\n\n// Request with unspecified values\nvar unspecifiedRequest = new UpdateUserRequest\n{\n    Email = default,\n    Age = default\n};\n\nvar resultUnspecified = validator.Validate(unspecifiedRequest);\n// resultUnspecified.IsValid == true\n// Validation rules are skipped for unspecified values\n```\n\n# Use Cases\n\n## API Patch Operations\n\nWhen updating resources via API endpoints, it's crucial to distinguish between fields that should be updated to `null` and fields that should remain unchanged.\n\n```csharp\npublic class UpdateUserRequest\n{\n    public OptionalValue\u003cstring?\u003e Email { get; set; }\n\n    public OptionalValue\u003cstring?\u003e PhoneNumber { get; set; }\n}\n\n[HttpPatch(\"{id}\")]\npublic IActionResult UpdateUser(int id, UpdateUserRequest request)\n{\n    if (request.Email.IsSpecified)\n    {\n        // Update email to request.Email.SpecifiedValue\n    }\n\n    if (request.PhoneNumber.IsSpecified)\n    {\n        // Update phone number to request.PhoneNumber.SpecifiedValue\n    }\n\n    // Unspecified fields remain unchanged\n\n    return Ok();\n}\n```\n\n# Current Limitations\n\n- **DataAnnotations**: The `OptionalValue\u003cT\u003e` type does not support DataAnnotations validation attributes because they are tied to specific .NET Types (e.g. string).\n  - **\"Workaround\"**: Use the FluentValidation extensions to define validation rules for `OptionalValue\u003cT\u003e` properties.\n- **Support for other libraries**: Because `OptionalValue\u003cT\u003e` is a wrapper type it requires mapping to the underlying type for some libraries. Let me know if you have a specific library in mind that you would like to see support for.\n\n# Contributing\n\nContributions are welcome! Please feel free to submit issues or pull requests on the [GitHub repository](https://github.com/desjoerd/OptionalValues).\n\n# License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n\n# Benchmarks\n\nThe project is benchmarked with [BenchmarkDotNet](https://benchmarkdotnet.org/) to check any additional overhead that the `OptionalValue\u003cT\u003e` type might introduce. They are located in the `/test/OptionalValues.Benchmarks` directory.\n\nBelow are the results of the benchmarks for the `OptionalValue\u003cT\u003e` serialization performance on my machine:\n\n```\n\nBenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605)\n13th Gen Intel Core i9-13900H, 1 CPU, 20 logical and 14 physical cores\n.NET SDK 9.0.101\n  [Host]     : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2\n  DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2\n\n\n```\n| Method                                         |      Mean |    Error |   StdDev | Ratio | RatioSD |   Gen0 | Allocated | Alloc Ratio |\n| ---------------------------------------------- | --------: | -------: | -------: | ----: | ------: | -----: | --------: | ----------: |\n| SerializePrimitiveModel                        | 102.10 ns | 1.296 ns | 1.212 ns |  1.00 |    0.02 | 0.0088 |     112 B |        1.00 |\n| SerializeOptionalValueModel                    | 108.55 ns | 1.324 ns | 1.238 ns |  1.06 |    0.02 | 0.0134 |     168 B |        1.50 |\n| SerializePrimitiveModelWithSourceGenerator     |  75.65 ns | 1.554 ns | 1.727 ns |  0.74 |    0.02 | 0.0088 |     112 B |        1.00 |\n| SerializeOptionalValueModelWithSourceGenerator |  93.47 ns | 1.690 ns | 1.581 ns |  0.92 |    0.02 | 0.0134 |     168 B |        1.50 |\n\n*1ns = 1/1,000,000,000 seconds*\n\nIt is comparing the serialization performance between these two models:\n```csharp\npublic class PrimitiveModel\n{\n    public int Age { get; set; } = 42;\n    public string FirstName { get; set; } = \"John\";\n    public string? LastName { get; set; } = null;\n}\n\npublic class OptionalValueModel\n{\n    public OptionalValue\u003cint\u003e Age { get; set; } = 42;\n    public OptionalValue\u003cstring\u003e FirstName { get; set; } = \"John\";\n    public OptionalValue\u003cstring\u003e LastName { get; set; } = default;\n}\n```","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdesjoerd%2Foptionalvalues","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdesjoerd%2Foptionalvalues","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdesjoerd%2Foptionalvalues/lists"}