Skip to content

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 ​

shell
dotnet add package Tayra.Json
powershell
Install-Package Tayra.Json

Setup ​

There are two ways to register Tayra with System.Text.Json: through dependency injection or directly on a JsonSerializerOptions instance.

Register Tayra core services, a key store, and the JSON converter factory in your DI container:

cs
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
});
anchor

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:

cs
// 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);
anchor

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:

cs
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);
anchor

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 ​

cs
services.AddTayraJson(opts =>
{
    opts.EncryptOnSerialize = true;   // default
    opts.DecryptOnDeserialize = true; // default
    opts.ConfigureAspNetCore = true;  // default
});
anchor
PropertyTypeDefaultDescription
EncryptOnSerializebooltrueEncrypt [PersonalData] fields before writing JSON output
DecryptOnDeserializebooltrueDecrypt [PersonalData] fields after reading JSON input
ConfigureAspNetCorebooltrueIn 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:

cs
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
}
anchor
  • [DataSubjectId] on CustomerId tells Tayra to derive the encryption key from this value.
  • [PersonalData] on Name and Email marks them for encryption.
  • The ReplacementValue parameter on Email specifies the value returned after crypto-shredding.
  • AccountType has no attribute and is never touched by Tayra.

How It Works ​

The TayraJsonConverterFactory intercepts System.Text.Json serialization at the converter level:

  1. Type inspection - The factory consults the PersonalDataMetadataCache to determine if a type has any [PersonalData] properties. If a type has no PII fields, the factory returns false from CanConvert() and System.Text.Json uses its default serialization.

  2. 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.

  3. 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:

cs
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
});
anchor

Event Sourcing Payloads ​

Protect PII in event payloads written to event stores, message queues, or log files:

cs
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);
anchor

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:

cs
var json = JsonSerializer.Serialize(payload, jsonOptions);
await httpClient.PostAsync("/api/process", new StringContent(json, Encoding.UTF8, "application/json"));
anchor

File Storage ​

Protect PII when exporting data to JSON files for backup, reporting, or data exchange:

cs
var customers = await db.Customers.ToListAsync();
var json = JsonSerializer.Serialize(customers, jsonOptions);
await File.WriteAllTextAsync("export.json", json);
// PII fields are encrypted in the file
anchor

Crypto-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:

cs
// 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"
anchor

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 ​