Loading

Serialization

Elastic.Ingest.Elasticsearch uses System.Text.Json for all serialization. It supports custom serialization contexts for AOT scenarios and custom event writers for full control over document serialization.

By default, documents are serialized using System.Text.Json.JsonSerializer with these options:

  • DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault (omits properties with default values)
  • Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping (fast encoding, safe for Elasticsearch)

The channel serializes documents into NDJSON format for the _bulk API: each document becomes a pair of lines (operation header + document body).

For Native AOT or trimming scenarios, provide a JsonSerializerContext to enable source-generated serialization:

[JsonSerializable(typeof(Product))]
public partial class MySerializerContext : JsonSerializerContext { }
		

Set it on the channel options:

var options = new IngestChannelOptions<Product>(transport, MyContext.Product.Context)
{
    SerializerContext = MySerializerContext.Default
};
		

For multiple document types, use SerializerContexts:

options.SerializerContexts = new JsonSerializerContext[]
{
    ProductSerializerContext.Default,
    OrderSerializerContext.Default
};
		

The library combines your contexts with its internal contexts (IngestSerializationContext for bulk operations, ElasticsearchTransportSerializerContext for transport types) using JsonTypeInfoResolver.Combine.

For full control over how documents are serialized, implement IElasticsearchEventWriter<TEvent>:

public class CustomWriter : IElasticsearchEventWriter<Product>
{
    // Stream-based writing (all .NET versions)
    public Func<Stream, Product, CancellationToken, Task>? WriteToStreamAsync { get; set; }
        = async (stream, product, ctx) =>
        {
            // Custom serialization logic
            await JsonSerializer.SerializeAsync(stream, product, ctx);
        };

    // Memory-based writing (.NET Standard 2.1+ / .NET 8.0+)
    public Action<ArrayBufferWriter<byte>, Product>? WriteToArrayBuffer { get; set; }
        = (buffer, product) =>
        {
            // Custom serialization logic using Utf8JsonWriter
            using var writer = new Utf8JsonWriter(buffer);
            // ... write JSON
        };
}
		

Set it on the channel options:

var options = new IngestChannelOptions<Product>(transport, MyContext.Product.Context)
{
    EventWriter = new CustomWriter()
};
		

When an event writer is set, the channel uses it instead of JsonSerializer for the document body. The bulk operation header is still serialized by the library.

The full serialization flow for each document in a bulk request:

  1. Operation header: serialized by the library (for example, {"index":{"_index":"products","_id":"ABC"}})
  2. Newline: \n
  3. Document body: serialized by your event writer (if set) or JsonSerializer
  4. Newline: \n

For update operations, the document body is wrapped in {"doc_as_upsert":true,"doc":...} automatically.