System.Text.Json
Tayra integrates with System.Text.Json through a custom JsonConverterFactory that automatically encrypts [PersonalData] fields during JSON serialization and decrypts them during deserialization. Annotate once, protect everywhere - the same attributes that protect your data in EF Core, Marten, and messaging also protect it in JSON serialization.
Install
dotnet add package Tayra.JsonInstall-Package Tayra.JsonSetup
There are two ways to register Tayra with System.Text.Json: through dependency injection or directly on a JsonSerializerOptions instance.
DI-Based Setup (Recommended)
Register Tayra core services, a key store, and the JSON converter factory in your DI container:
services.AddTayra(opts => opts.LicenseKey = licenseKey); // defaults to InMemoryKeyStore; use PostgreSQL, Vault, etc. in production
services.AddTayraJson(opts =>
{
opts.EncryptOnSerialize = true; // default
opts.DecryptOnDeserialize = true; // default
opts.ConfigureAspNetCore = true; // default
});AddTayraJson() registers a configured TayraJsonConverterFactory as a singleton. In an ASP.NET Core app it also adds the factory to the JSON options of minimal APIs (Microsoft.AspNetCore.Http.Json.JsonOptions) and MVC controllers (Microsoft.AspNetCore.Mvc.JsonOptions), so request bodies are decrypted and responses encrypted with no further code. Outside ASP.NET Core it only registers the factory; Tayra.Json does not depend on ASP.NET Core.
To keep ASP.NET Core serialization as it is, opt out:
// Keep ASP.NET Core responses in plaintext, for example when the app masks them with
// Tayra.AspNetCore's response scrubbing instead.
services.AddTayraJson(opts => opts.ConfigureAspNetCore = false);Encryption and response scrubbing are alternatives
With the converter wired in, responses carry ciphertext. If the app instead masks personal data in responses with Tayra.AspNetCore's response scrubbing, set ConfigureAspNetCore = false: the scrubber should mask plaintext, not ciphertext.
Standalone Setup
If you prefer to configure JsonSerializerOptions directly (e.g., in a console app or library), call UseTayra() on the options instance:
var tayra = serviceProvider.GetRequiredService<ITayra>();
var metadataCache = serviceProvider.GetRequiredService<PersonalDataMetadataCache>();
var jsonOptions = new JsonSerializerOptions();
jsonOptions.UseTayra(tayra, metadataCache);
var json = JsonSerializer.Serialize(customer, jsonOptions);
var deserialized = JsonSerializer.Deserialize<Customer>(json, jsonOptions);When to Use Standalone vs DI
Use the DI-based setup when you have a standard ASP.NET Core or hosted service application. Use the standalone setup when you need fine-grained control or are configuring serialization outside of DI (e.g., in a migration tool, file export utility, or console app).
Options
services.AddTayraJson(opts =>
{
opts.EncryptOnSerialize = true; // default
opts.DecryptOnDeserialize = true; // default
opts.ConfigureAspNetCore = true; // default
});| Property | Type | Default | Description |
|---|---|---|---|
EncryptOnSerialize | bool | true | Encrypt [PersonalData] fields before writing JSON output |
DecryptOnDeserialize | bool | true | Decrypt [PersonalData] fields after reading JSON input |
ConfigureAspNetCore | bool | true | In an ASP.NET Core app, add the converter to the minimal API and MVC JSON options. No effect elsewhere |
Define Your Model
Annotate your types with [DataSubjectId] and [PersonalData] to enable automatic encryption:
public class Customer
{
[DataSubjectId]
public string CustomerId { get; set; } = "";
[PersonalData]
public string Name { get; set; } = "";
[PersonalData(ReplacementValue = "redacted@example.com")]
public string Email { get; set; } = "";
public string AccountType { get; set; } = ""; // Not encrypted
}[DataSubjectId]onCustomerIdtells Tayra to derive the encryption key from this value.[PersonalData]onNameandEmailmarks them for encryption.- The
ReplacementValueparameter onEmailspecifies the value returned after crypto-shredding. AccountTypehas no attribute and is never touched by Tayra.
How It Works
The TayraJsonConverterFactory intercepts System.Text.Json serialization at the converter level:
Type inspection - The factory consults the
PersonalDataMetadataCacheto determine if a type has any[PersonalData]properties. If a type has no PII fields, the factory returnsfalsefromCanConvert()andSystem.Text.Jsonuses its default serialization.On serialize - When writing JSON, the converter encrypts all
[PersonalData]fields on the object, serializes the object to JSON (with ciphertext values), and then puts the object's plaintext back exactly as it was. Your application code continues working with plaintext values after serialization. The restore also happens when serialization throws, so serializing the object again encrypts it once rather than twice.On deserialize - When reading JSON, the converter deserializes the JSON into an object (with ciphertext values in the PII fields), and then decrypts those fields. The returned object contains cleartext values.
Synchronous Bridge
System.Text.Json converters run synchronously, but Tayra's encryption engine is async. The converter uses .GetAwaiter().GetResult() to bridge this gap - the same pattern used by Tayra's EF Core materialization interceptor. Because the crypto engine caches keys in memory, the synchronous call completes as a memory-only lookup after the initial key fetch.
Use Cases
API Responses
In an ASP.NET Core app, AddTayraJson() wires the converter into the minimal API and MVC JSON options (see DI-Based Setup), so all JSON responses containing annotated types are encrypted:
app.MapGet("/api/customers/{id}", async (string id, CustomerService svc) =>
{
var customer = await svc.GetCustomerAsync(id);
return Results.Ok(customer);
// Name and Email are encrypted in the JSON response
});Event Sourcing Payloads
Protect PII in event payloads written to event stores, message queues, or log files:
var evt = new CustomerRegisteredEvent
{
CustomerId = "cust-42",
Name = "Alice Smith",
Email = "alice@example.com",
};
var json = JsonSerializer.Serialize(evt, jsonOptions);
// json contains AES-256-GCM ciphertext for Name and Email
await eventStore.AppendAsync("cust-42", json);Inter-Service Communication
When services exchange JSON payloads over HTTP, gRPC, or message queues, Tayra ensures PII is protected in transit without requiring transport-level encryption of individual fields:
var json = JsonSerializer.Serialize(payload, jsonOptions);
await httpClient.PostAsync("/api/process", new StringContent(json, Encoding.UTF8, "application/json"));File Storage
Protect PII when exporting data to JSON files for backup, reporting, or data exchange:
var customers = await db.Customers.ToListAsync();
var json = JsonSerializer.Serialize(customers, jsonOptions);
await File.WriteAllTextAsync("export.json", json);
// PII fields are encrypted in the fileCrypto-Shredding
GDPR Article 17 "right to be forgotten" works seamlessly with JSON serialization. After deleting a data subject's encryption key, any JSON that was serialized with encrypted PII becomes permanently unreadable:
// Serialize with encryption
var json = JsonSerializer.Serialize(customer, jsonOptions);
await File.WriteAllTextAsync("customer.json", json);
// Later: erase the customer's data
await tayra.ShredAsync("cust-42");
// Deserialize after shredding: returns replacement values
var shredded = JsonSerializer.Deserialize<Customer>(
await File.ReadAllTextAsync("customer.json"), jsonOptions);
// shredded.Name == ""
// shredded.Email == "redacted@example.com"Irreversible
Crypto-shredding is permanent. The encryption key is deleted from the key store and cannot be recovered. Any JSON containing the shredded subject's encrypted fields will return replacement values forever.
See Also
- Getting Started - End-to-end encryption tutorial
- ASP.NET Core Integration - Response scrubbing middleware
- EF Core Integration - Database-level encryption
- Key Stores - Production key store options
