Optional values for C# to model omitted values (undefined in javascript) and it has a JsonConvertor to support omitted values in Json.
$ dotnet add package OptionalValuesA .NET library that provides an OptionalValue<T> type, representing a value that may or may not be specified, with comprehensive support for JSON serialization. e.g. (undefined, null, "value")
| Package | Version |
|---|---|
| OptionalValues | |
| OptionalValues.OpenApi | |
| OptionalValues.Swashbuckle | |
| OptionalValues.NSwag | |
| OptionalValues.DataAnnotations | |
| OptionalValues.FluentValidation |
The OptionalValue<T> struct is designed to represent a value that can be in one of three states:
undefined)not null.null value: The value has been specified and is null.When 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<T> you can distinguish between null and Unspecified values.
using System.Text.Json;
using OptionalValues;
var jsonSerializerOptions = new JsonSerializerOptions()
.AddOptionalValueSupport();
var json =
"""
{
"FirstName": "John",
"LastName": null
}
""";
var person1 = JsonSerializer.Deserialize<Person>(json, jsonSerializerOptions);
// equals:
var person2 = new Person
{
FirstName = "John",
LastName = null,
Address = OptionalValue<string>.Unspecified // or default
};
bool areEqual = person1 == person2; // True
string serialized = JsonSerializer.Serialize(person2, jsonSerializerOptions);
// Output: {"FirstName":"John","LastName":null}
public record Person
{
public OptionalValue<string> FirstName { get; set; }
public OptionalValue<string?> LastName { get; set; }
public OptionalValue<string> Address { get; set; }
}
Install the package using the .NET CLI:
dotnet add package OptionalValues
For JSON serialization support, configure the JsonSerializerOptions to include the OptionalValue<T> converter:
var options = new JsonSerializerOptions()
.AddOptionalValueSupport();
Optionally, install one or more extension packages:
dotnet add package OptionalValues.Swashbuckle
dotnet add package OptionalValues.NSwag
dotnet add package OptionalValues.DataAnnotations
dotnet add package OptionalValues.FluentValidation
null versus when it has not been specified at all. This allows for mapping undefined values in JSON to Unspecified values in C#.OptionalValue<T>, including GetOptionalValue, AddOptionalValue, TryAddOptionalValue, and SetOptionalValue.OptionalValue<T> properties.OptionalValue<T> properties using FluentValidation.OptionalValues.OpenApi package. It provides a schema transformer to correctly handle OptionalValue<T> types.OptionalValues.NSwag package. It includes an OptionalValueTypeMapper to map the OptionalValue<T> to its underlying type T in the generated OpenAPI schema.null or remain unchanged.You can create an OptionalValue<T> in several ways:
Unspecified Value:
var unspecified = new OptionalValue<string>();
// or
var unspecified = OptionalValue<string>.Unspecified;
// or
OptionalValue<string> unspecified = default;
Specified Value:
var specifiedValue = new OptionalValue<string>("Hello, World!");
// or using implicit conversion
OptionalValue<string> specifiedValue = "Hello, World!";
Specified Null Value:
var specifiedNull = new OptionalValue<string?>(null);
// or using implicit conversion
OptionalValue<string?> specifiedNull = null;
Use the IsSpecified property to determine if the value has been specified:
if (optionalValue.IsSpecified)
{
Console.WriteLine("Value is specified.");
}
else
{
Console.WriteLine("Value is unspecified.");
}
.Value: Gets the value if specified; returns null if unspecified..SpecifiedValue: Gets the specified value; throws InvalidOperationException if the value is unspecified..GetSpecifiedValueOrDefault(): Gets the specified value or the default value of T if unspecified..GetSpecifiedValueOrDefault(T defaultValue): Gets the specified value or the provided default value if unspecified.var optionalValue = new OptionalValue<string>("Example");
// Using Value
string? value = optionalValue.Value;
// Using SpecifiedValue
string specifiedValue = optionalValue.SpecifiedValue;
// Using GetSpecifiedValueOrDefault
string valueOrDefault = optionalValue.GetSpecifiedValueOrDefault("Default Value");
OptionalValue<T> supports implicit conversions to and from T:
// From T to OptionalValue<T>
OptionalValue<int> optionalInt = 42;
// From OptionalValue<T> to T (returns null if unspecified)
int? value = optionalInt;
Equality checks consider both the IsSpecified property and the Value:
var value1 = new OptionalValue<string>("Test");
var value2 = new OptionalValue<string>("Test");
var unspecified = new OptionalValue<string>();
bool areEqual = value1 == value2; // True
bool areUnspecifiedEqual = unspecified == new OptionalValue<string>(); // True
Extension methods for working with dictionaries and OptionalValue<T>:
using OptionalValues.Extensions;
var settings = new Dictionary<string, int> { ["timeout"] = 30 };
// Get value as OptionalValue (returns Unspecified if key not found)
OptionalValue<int> timeout = settings.GetOptionalValue("timeout"); // IsSpecified == true
OptionalValue<int> retries = settings.GetOptionalValue("retries"); // IsSpecified == false
// Add/Set only when value is specified
settings.AddOptionalValue("maxRetries", new OptionalValue<int>(3)); // Adds the value
settings.AddOptionalValue("other", OptionalValue<int>.Unspecified); // Does nothing
settings.SetOptionalValue("timeout", new OptionalValue<int>(60)); // Updates to 60
OptionalValue<T> includes a custom JSON converter and JsonTypeInfoResolver Modifier to handle serialization and deserialization of optional values.
To properly serialize OptionalValue<T> properties, add it to the JsonSerializerOptions:
var newOptionsWithSupport = JsonSerializerOptions.Default
.WithOptionalValueSupport();
// or
var options = new JsonSerializerOptions();
options.AddOptionalValueSupport();
null value.public class Person
{
public OptionalValue<string> FirstName { get; set; }
public OptionalValue<string> LastName { get; set; }
}
// Creating a Person instance
var person = new Person
{
FirstName = "John", // Specified non-null value
LastName = new OptionalValue<string>() // Unspecified
};
// Serializing to JSON
string json = JsonSerializer.Serialize(person);
// Output: {"FirstName":"John"}
null: Deserialized as specified with a null value.string jsonInput = @"{""FirstName"":""John"",""LastName"":null}";
var person = JsonSerializer.Deserialize<Person>(jsonInput);
bool isFirstNameSpecified = person.FirstName.IsSpecified; // True
string firstName = person.FirstName.SpecifiedValue; // "John"
bool isLastNameSpecified = person.LastName.IsSpecified; // True
string lastName = person.LastName.SpecifiedValue; // null
OptionalValue<T> 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.
JsonSerializerOptions Options = new JsonSerializerOptions
{
RespectNullableAnnotations = true,
}.AddOptionalValueSupport();
var json = """
{
"NotNullable": null
}
""";
var model = JsonSerializer.Deserialize<Model>(json, Options); // Throws JsonException
private class Model
{
public OptionalValue<string> NotNullable { get; init; }
}
There are a few limitations to this feature:
// 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.
public class Model<T>
{
public OptionalValue<T> NotNullable { get; init; }
}
The OptionalValues library integrates seamlessly with ASP.NET Core, allowing you to use OptionalValue<T> properties in your API models.
You only need to configure the JsonSerializerOptions to include the OptionalValue<T> converter:
// For Minimal API
builder.Services.ConfigureHttpJsonOptions(jsonOptions =>
{
// Make sure that AddOptionalValueSupport() is the last call when you are using the `TypeInfoResolverChain` of the `SerializerOptions`.
jsonOptions.SerializerOptions.AddOptionalValueSupport();
});
// For MVC
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.AddOptionalValueSupport();
});
The OptionalValues.OpenApi package provides support for ASP.NET Core's built-in OpenAPI support (Microsoft.AspNetCore.OpenApi) to generate accurate OpenAPI documentation for OptionalValue<T> properties.
It correctly unwraps the OptionalValue<T> type and generates the appropriate schema for the underlying type T.
Install the package using the .NET CLI:
dotnet add package OptionalValues.OpenApi
Configure the OpenAPI services to use the OptionalValue<T> schema transformer:
builder.Services.AddOpenApi(options =>
{
options.AddOptionalValueSupport();
});
The OptionalValues.Swashbuckle package provides a custom data contract resolver for Swashbuckle to generate accurate OpenAPI/Swagger documentation for OptionalValue<T> properties.
It correctly unwraps the OptionalValue<T> type and generates the appropriate schema for the underlying type T.
Install the package using the .NET CLI:
dotnet add package OptionalValues.Swashbuckle
Configure the Swashbuckle services to use the OptionalValueDataContractResolver:
builder.Services.AddSwaggerGen();
// after AddSwaggerGen when you want it to use an existing custom ISerializerDataContractResolver.
builder.Services.AddSwaggerGenOptionalValueSupport();
The OptionalValues.NSwag package provides an OptionalValueTypeMapper to map the OptionalValue<T> to its underlying type T in the generated OpenAPI schema.
Install the package using the .NET CLI:
dotnet add package OptionalValues.NSwag
Configure the NSwag SchemaSettings to use the OptionalValueTypeMapper:
builder.Services.AddOpenApiDocument(options =>
{
// Add OptionalValue support to NSwag
options.SchemaSettings.AddOptionalValueSupport();
});
The OptionalValues.DataAnnotations package provides DataAnnotations validation attributes for OptionalValue<T> 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).
Install the package using the .NET CLI:
dotnet add package OptionalValues.DataAnnotations
Presence Validators:
[Specified]: Ensures the OptionalValue<T> is specified (present), but allows null or empty values.[RequiredValue]: Ensures the OptionalValue<T> is specified and its value is not null or empty. This should be used instead of the standard [Required] attribute.Example usage:
public class ExampleModel
{
[OptionalAllowedValues("a")]
public OptionalValue<string> AllowedValues { get; set; }
[OptionalDeniedValues("a")]
public OptionalValue<string> DeniedValues { get; set; }
[OptionalLength(1, 5)]
public OptionalValue<int[]> LengthCollection { get; set; }
[OptionalLength(1, 5)]
public OptionalValue<string> LengthString { get; set; }
[OptionalMaxLength(5)]
public OptionalValue<int[]> MaxLengthCollection { get; set; }
[OptionalMaxLength(5)]
public OptionalValue<string> MaxLengthString { get; set; }
[OptionalMinLength(1)]
public OptionalValue<int[]> MinLengthCollection { get; set; }
[OptionalMinLength(1)]
public OptionalValue<string> MinLengthString { get; set; }
[OptionalRange(5, 42)]
public OptionalValue<int> Range { get; set; }
[OptionalRegularExpression("^something$")]
public OptionalValue<string> RegularExpression { get; set; }
[Specified]
public OptionalValue<string?> Specified { get; set; }
[RequiredValue]
public OptionalValue<string> SpecifiedRequired { get; set; }
[OptionalStringLength(5)]
public OptionalValue<string> StringLength { get; set; }
}
The OptionalValues.FluentValidation package provides extension methods to simplify the validation of OptionalValue<T> properties using FluentValidation.
Install the package using the .NET CLI:
dotnet add package OptionalValues.FluentValidation
The OptionalRuleFor extension method allows you to define validation rules for OptionalValue<T> properties that are only applied when the value is specified.
using FluentValidation;
using OptionalValues.FluentValidation;
public class UpdateUserRequest
{
public OptionalValue<string?> Email { get; set; }
public OptionalValue<int> Age { get; set; }
}
public class UpdateUserRequestValidator : AbstractValidator<UpdateUserRequest>
{
public UpdateUserRequestValidator()
{
this.OptionalRuleFor(x => x.Email, x => x
.NotEmpty()
.EmailAddress());
this.OptionalRuleFor(x => x.Age, x => x
.GreaterThan(18));
}
}
In this example:
Email and Age are applied only if the corresponding OptionalValue<T> is specified.The OptionalRuleFor method:
OptionalValue<T> property.IsSpecified) before applying the validation rules.var validator = new UpdateUserRequestValidator();
// Valid request with specified values
var validRequest = new UpdateUserRequest
{
Email = "user@example.com",
Age = 25
};
var result = validator.Validate(validRequest);
// result.IsValid == true
// Invalid request with specified values
var invalidRequest = new UpdateUserRequest
{
Email = "invalid-email",
Age = 17
};
var resultInvalid = validator.Validate(invalidRequest);
// resultInvalid.IsValid == false
// Errors for Email and Age
// Request with unspecified values
var unspecifiedRequest = new UpdateUserRequest
{
Email = default,
Age = default
};
var resultUnspecified = validator.Validate(unspecifiedRequest);
// resultUnspecified.IsValid == true
// Validation rules are skipped for unspecified values
When updating resources via API endpoints, it's crucial to distinguish between fields that should be updated to null and fields that should remain unchanged.
public class UpdateUserRequest
{
public OptionalValue<string?> Email { get; set; }
public OptionalValue<string?> PhoneNumber { get; set; }
}
[HttpPatch("{id}")]
public IActionResult UpdateUser(int id, UpdateUserRequest request)
{
if (request.Email.IsSpecified)
{
// Update email to request.Email.SpecifiedValue
}
if (request.PhoneNumber.IsSpecified)
{
// Update phone number to request.PhoneNumber.SpecifiedValue
}
// Unspecified fields remain unchanged
return Ok();
}
OptionalValue<T> type does not support DataAnnotations validation attributes because they are tied to specific .NET Types (e.g. string).
OptionalValue<T> properties.OptionalValue<T> 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.Contributions are welcome! Please feel free to submit issues or pull requests on the GitHub repository.
This project is licensed under the MIT License - see the LICENSE file for details.
The project is benchmarked with BenchmarkDotNet to check any additional overhead that the OptionalValue<T> type might introduce. They are located in the /test/OptionalValues.Benchmarks directory.
Below are the results of the benchmarks for the OptionalValue<T> serialization performance on my machine:
BenchmarkDotNet v0.14.0, Windows 11 (10.0.26100.2605)
13th Gen Intel Core i9-13900H, 1 CPU, 20 logical and 14 physical cores
.NET SDK 9.0.101
[Host] : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
DefaultJob : .NET 9.0.0 (9.0.24.52809), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|
| SerializePrimitiveModel | 102.10 ns | 1.296 ns | 1.212 ns | 1.00 | 0.02 | 0.0088 | 112 B | 1.00 |
| SerializeOptionalValueModel | 108.55 ns | 1.324 ns | 1.238 ns | 1.06 | 0.02 | 0.0134 | 168 B | 1.50 |
| SerializePrimitiveModelWithSourceGenerator | 75.65 ns | 1.554 ns | 1.727 ns | 0.74 | 0.02 | 0.0088 | 112 B | 1.00 |
| SerializeOptionalValueModelWithSourceGenerator | 93.47 ns | 1.690 ns | 1.581 ns | 0.92 | 0.02 | 0.0134 | 168 B | 1.50 |
1ns = 1/1,000,000,000 seconds
It is comparing the serialization performance between these two models:
public class PrimitiveModel
{
public int Age { get; set; } = 42;
public string FirstName { get; set; } = "John";
public string? LastName { get; set; } = null;
}
public class OptionalValueModel
{
public OptionalValue<int> Age { get; set; } = 42;
public OptionalValue<string> FirstName { get; set; } = "John";
public OptionalValue<string> LastName { get; set; } = default;
}