Zentient Results Api Reference Serialization - ulfbou/Zentient.Results GitHub Wiki
Serializing Result and Result<T> types is crucial for scenarios like building APIs (returning JSON responses), logging, inter-service communication (e.g., message queues), and persistence. This section outlines how Zentient.Results types behave with common JSON serializers, specifically focusing on System.Text.Json.
Zentient.Results types (Result, Result<T>, ErrorInfo, ResultStatus) are designed to be largely compatible with standard JSON serializers due to their immutable nature and use of public properties.
System.Text.Json is the default JSON serializer in modern .NET applications.
When you directly use the concrete Result or Result<T> structs, System.Text.Json can usually serialize them without special configuration. The properties (IsSuccess, IsFailure, Errors, Messages, Status, Value) will be serialized as standard JSON fields.
Key considerations:
-
Value: ForResult<T>, ifIsFailureistrue,Valuewill bedefault(T)(e.g.,nullfor reference types,0forint).System.Text.Jsonwill serialize this asnullor0respectively. -
ErrorsandMessages: These collections will be serialized as JSON arrays. -
ErrorInfoandResultStatus: These are alsoreadonly structtypes with public properties, so they serialize cleanly.
Example: Serializing Result<User>
using Zentient.Results;
using System.Text.Json;
using System.Text.Json.Serialization; // For optional attributes
using System.Collections.Generic;
public record User(int Id, string Name, string Email);
// --- Successful Result ---
IResult<User> successResult = Result.Success(
new User(1, "Alice", "[email protected]"),
"User loaded successfully."
);
string jsonSuccess = JsonSerializer.Serialize(successResult, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine("--- Successful Result JSON ---");
Console.WriteLine(jsonSuccess);
/* Expected Output:
--- Successful Result JSON ---
{
"Value": {
"Id": 1,
"Name": "Alice",
"Email": "[email protected]"
},
"IsSuccess": true,
"IsFailure": false,
"Errors": [],
"Messages": [
"User loaded successfully."
],
"Error": null,
"Status": {
"Code": 200,
"Description": "OK",
"IsSuccess": true
}
}
*/
// --- Failed Result ---
IResult<User> failureResult = Result.NotFound<User>("User", "999");
string jsonFailure = JsonSerializer.Serialize(failureResult, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine("\n--- Failed Result JSON ---");
Console.WriteLine(jsonFailure);
/* Expected Output:
--- Failed Result JSON ---
{
"Value": null, // 'Value' is null for reference type T when failed
"IsSuccess": false,
"IsFailure": true,
"Errors": [
{
"Category": 4, // Corresponds to ErrorCategory.NotFound enum value
"Code": "UserNotFound",
"Message": "Resource 'User' with identifier '999' not found.",
"Parameters": {
"resourceName": "User",
"identifier": "999"
}
}
],
"Messages": [],
"Error": "Resource 'User' with identifier '999' not found.",
"Status": {
"Code": 404,
"Description": "Not Found",
"IsSuccess": false
}
}
*/Deserialization also works well for concrete Result and Result<T> types. System.Text.Json can correctly map the JSON properties back to the struct's fields.
Example: Deserializing Result<User>
using Zentient.Results;
using System.Text.Json;
using System.Collections.Generic;
public record User(int Id, string Name, string Email);
// --- Deserializing Successful Result ---
string jsonSuccess = @"{
""Value"": { ""Id"": 1, ""Name"": ""Alice"", ""Email"": ""[email protected]"" },
""IsSuccess"": true,
""IsFailure"": false,
""Errors"": [],
""Messages"": [""User loaded successfully.""],
""Error"": null,
""Status"": { ""Code"": 200, ""Description"": ""OK"", ""IsSuccess"": true }
}";
IResult<User>? deserializedSuccess = JsonSerializer.Deserialize<Result<User>>(jsonSuccess);
Console.WriteLine("\n--- Deserialized Successful Result ---");
Console.WriteLine($"IsSuccess: {deserializedSuccess?.IsSuccess}");
Console.WriteLine($"Value: {deserializedSuccess?.Value?.Name}");
Console.WriteLine($"Status: {deserializedSuccess?.Status?.Description}");
// --- Deserializing Failed Result ---
string jsonFailure = @"{
""Value"": null,
""IsSuccess"": false,
""IsFailure"": true,
""Errors"": [
{
""Category"": 4,
""Code"": ""UserNotFound"",
""Message"": ""Resource 'User' with identifier '999' not found."",
""Parameters"": { ""resourceName"": ""User"", ""identifier"": ""999"" }
}
],
""Messages"": [],
""Error"": ""Resource 'User' with identifier '999' not found."",
""Status"": { ""Code"": 404, ""Description"": ""Not Found"", ""IsSuccess"": false }
}";
IResult<User>? deserializedFailure = JsonSerializer.Deserialize<Result<User>>(jsonFailure);
Console.WriteLine("\n--- Deserialized Failed Result ---");
Console.WriteLine($"IsSuccess: {deserializedFailure?.IsSuccess}");
Console.WriteLine($"Error: {deserializedFailure?.Error}");
Console.WriteLine($"Status: {deserializedFailure?.Status?.Description}");
Console.WriteLine($"Error Category: {deserializedFailure?.Errors.FirstOrDefault()?.Category}");Directly serializing or deserializing interfaces like IResult or IResult<T> with System.Text.Json requires additional configuration due to its emphasis on strict type handling.
By default, System.Text.Json cannot deserialize an interface directly because it doesn't know which concrete type to instantiate. When serializing an interface, it typically only serializes the public properties defined on the interface itself.
To handle polymorphism (e.g., if your API controller returns IResult<User> and you want to deserialize it back as Result<User>), you have two primary options:
-
Use
JsonDerivedType(for .NET 7+): This attribute on the base interface allows you to specify derived types for polymorphic deserialization. This is the cleanest approach for modern .NET.// In your project, you'd add this to a shared IResult interface or a custom IResult interface you define. // However, for Zentient.Results' built-in IResult, you cannot directly add attributes. // If you *must* deserialize to an interface type, you'd need a custom JsonConverter. // Example of how JsonDerivedType *would* work if you controlled the IResult definition: /* [JsonDerivedType(typeof(Result), typeDiscriminator: "nonGenericResult")] [JsonDerivedType(typeof(Result<>), typeDiscriminator: "genericResult")] // This syntax doesn't quite work for open generics like Result<T> public interface IResult { /* ... */ } */ // For Zentient.Results, it's generally best to: // a) Return/Expect concrete Result/Result<T> types in API contracts. // b) Use a custom JsonConverter for IResult/IResult<T> if you insist on using interfaces in contracts. // c) Use Newtonsoft.Json if you need simpler polymorphic handling out-of-the-box (see next section).
-
Implement a Custom
JsonConverter<T>: For complex scenarios or if you need to deserialize to interface types whereJsonDerivedTypeisn't feasible (likeZentient.Results' built-in interfaces), you can write a customJsonConverterthat handles the logic of determining the concrete type (e.g., based onIsSuccessorIsFailureproperties) during deserialization. This is more involved.// This is an example of a simple custom converter. // A robust converter would need to handle all properties of Result and Result<T> correctly, // including generic types. This is non-trivial for Result<T>. /* public class IResultConverter : JsonConverter<IResult> { public override IResult Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Read the JSON object using (JsonDocument doc = JsonDocument.ParseValue(ref reader)) { var root = doc.RootElement; if (root.TryGetProperty("IsSuccess", out JsonElement isSuccessElement) && isSuccessElement.GetBoolean()) { // It's a success result. Need to determine if it's generic. // This is where it gets complex for a single converter for both IResult and IResult<T>. // A more practical approach might be separate converters or using concrete types. // For simplicity, let's assume non-generic for this example: return JsonSerializer.Deserialize<Result>(root.GetRawText(), options); } else { // It's a failure result return JsonSerializer.Deserialize<Result>(root.GetRawText(), options); } } } public override void Write(Utf8JsonWriter writer, IResult value, JsonSerializerOptions options) { // Default serialization works for the concrete type JsonSerializer.Serialize(writer, (object)value, options); } } // Usage: // var options = new JsonSerializerOptions(); // options.Converters.Add(new IResultConverter()); // IResult? deserializedResult = JsonSerializer.Deserialize<IResult>(jsonString, options); */
Recommendation for System.Text.Json:
For most scenarios, it's simpler and more robust to serialize and deserialize directly using the concrete Result<T> or Result types in your API contracts (e.g., public Result<User> GetUser(int id)). If you absolutely need to use the IResult interface type in your API contract, consider returning a ProblemDetails (RFC 7807) for failures and the direct value for success if the framework allows, or implementing a robust custom JsonConverter (which is outside the scope of simple API reference).
Newtonsoft.Json (Json.NET) often provides more lenient out-of-the-box support for polymorphic serialization/deserialization, particularly when using TypeNameHandling.Objects ($type property).
-
Polymorphism: If you configure
TypeNameHandling.Objects, Newtonsoft.Json can embed type information ($typeproperty) that allows it to deserialize interfaces back to their concrete types. This comes with security implications, so use with caution and validation for untrusted inputs. -
Structs: Generally handles
readonly structtypes well.
Example with Newtonsoft.Json:
/*
using Newtonsoft.Json;
// --- Successful Result ---
IResult<User> successResult = Result.Success(
new User(1, "Alice", "[email protected]"),
"User loaded successfully."
);
string jsonSuccessNewtonsoft = JsonConvert.SerializeObject(successResult, Formatting.Indented);
Console.WriteLine("\n--- Successful Result JSON (Newtonsoft) ---");
Console.WriteLine(jsonSuccessNewtonsoft);
// --- Deserializing with TypeNameHandling (Newtonsoft) ---
// Note: This option has security implications and should be used carefully.
var settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
Formatting = Formatting.Indented
};
string jsonSuccessWithTypeName = JsonConvert.SerializeObject(successResult, settings);
Console.WriteLine("\n--- Success Result JSON (Newtonsoft with TypeNameHandling) ---");
Console.WriteLine(jsonSuccessWithTypeName);
// Deserializing back to IResult<User>
IResult<User>? deserializedSuccessNewtonsoft = JsonConvert.DeserializeObject<IResult<User>>(jsonSuccessWithTypeName, settings);
Console.WriteLine("\n--- Deserialized Successful Result (Newtonsoft to IResult<User>) ---");
Console.WriteLine($"IsSuccess: {deserializedSuccessNewtonsoft?.IsSuccess}");
Console.WriteLine($"Value: {deserializedSuccessNewtonsoft?.Value?.Name}");
// Ensure to have the Zentient.Results.Result<T> type visible to the deserializer
*/While default serialization handles most cases, you might require custom logic for:
-
Omitting properties: For example, always omitting the
Valueproperty ifIsFailureis true, even if it'sdefault(T). -
Specific formatting: Changing how
ErrorInfoorResultStatusare represented (e.g., flattening them). - Optimized payload: Creating a more compact JSON representation for network efficiency.
To achieve this, you would implement a custom JsonConverter<T> for the Result or Result<T> types. This involves overriding the Read and Write methods to control the serialization and deserialization process manually. However, due to the generic nature and internal structure of Result<T>, writing a comprehensive custom converter can be complex.
For most use cases, relying on the default serialization of the concrete Result and Result<T> structs is sufficient and recommended.