Skip to content

Commit 532c016

Browse files
committed
Adding "Registering custom System.Text.Json converters"
1 parent 9d9bccd commit 532c016

File tree

2 files changed

+141
-85
lines changed

2 files changed

+141
-85
lines changed

docs/client-concepts/serialization/custom-serialization.asciidoc

+48-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
The built-in source serializer handles most POCO document models correctly. Sometimes, you may need further control over how your types are serialized.
55

6-
NOTE: The built-in source serializer uses the Microsoft https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview[`System.Text.Json` library] internally. You can apply `System.Text.Json` attributes and converters to control serialization of your document types.
6+
NOTE: The built-in source serializer uses the https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview[Microsoft `System.Text.Json` library] internally. You can apply `System.Text.Json` attributes and converters to control the serialization of your document types.
77

88
[[system-text-json-attributes]]
99
===== Using `System.Text.Json` attributes
@@ -17,8 +17,8 @@ We can model a document to represent data about a person using a regular class (
1717
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=usings-serialization]
1818
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=person-class-with-attributes]
1919
----
20-
<1> The `JsonPropertyName` attribute is used to ensure the `FirstName` property uses the JSON property name `forename` when serialized.
21-
<2> The `JsonIgnore` attribute is used to prevent the `Age` property from appearing in the serialized JSON.
20+
<1> The `JsonPropertyName` attribute ensures the `FirstName` property uses the JSON name `forename` when serialized.
21+
<2> The `JsonIgnore` attribute prevents the `Age` property from appearing in the serialized JSON.
2222

2323
We can then index an instance of the document into {es}.
2424

@@ -37,15 +37,10 @@ The index request is serialized, with the source serializer handling the `Person
3737
}
3838
----
3939

40-
[[registering-custom-converters]]
41-
===== Registering custom `System.Text.Json` converters
42-
43-
TODO
44-
4540
[[configuring-custom-jsonserializeroptions]]
4641
===== Configuring custom `JsonSerializerOptions`
4742

48-
The default source serializer applies a set of standard `JsonSerializerOptions` when serializing source document types. In some circumstances, you may wish to override some of our defaults. This is possible by creating an instance of `DefaultSourceSerializer` and passing an `Action<JsonSerializerOptions>` which is applied after our defaults have been set. This mechanism allows you to apply additional settings, or change the value of our defaults.
43+
The default source serializer applies a set of standard `JsonSerializerOptions` when serializing source document types. In some circumstances, you may need to override some of our defaults. This is achievable by creating an instance of `DefaultSourceSerializer` and passing an `Action<JsonSerializerOptions>`, which is applied after our defaults have been set. This mechanism allows you to apply additional settings or change the value of our defaults.
4944

5045
The `DefaultSourceSerializer` includes a constructor that accepts the current `IElasticsearchClientSettings` and a `configureOptions` `Action`.
5146

@@ -61,7 +56,7 @@ Our application defines the following `Person` class, which models a document we
6156
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=person-class]
6257
----
6358

64-
We want to serialize our source document using Pascal Casing for the JSON properties. Since the options applied in the `DefaultSouceSerializer` set the `PropertyNamingPolicy` to `JsonNamingPolicy.CamelCase`, we must override this setting.
59+
We want to serialize our source document using Pascal Casing for the JSON properties. Since the options applied in the `DefaultSouceSerializer` set the `PropertyNamingPolicy` to `JsonNamingPolicy.CamelCase`, we must override this setting. After configuring the `ElasticsearchClientSettings`, we index our document to {es}.
6560

6661
[source,csharp]
6762
----
@@ -92,6 +87,49 @@ As an alternative to using a local function, we could store an `Action<JsonSeria
9287
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=custom-options-action]
9388
----
9489

90+
[[registering-custom-converters]]
91+
===== Registering custom `System.Text.Json` converters
92+
93+
In certain more advanced situations, you may have types which require further customization during serialization than is possible using `System.Text.Json` property attributes. In these cases, the recommendation from Microsoft is to leverage a custom `JsonConverter`. Source document types serialized using the `DefaultSourceSerializer` can leverage the power of custom converters.
94+
95+
For this example, our application has a document class that should use a legacy JSON structure to continue operating with existing indexed documents. Several options are available, but we'll apply a custom converter in this case.
96+
97+
Our class is defined, and the `JsonConverter` attribute is applied to the class type, specifying the type of a custom converter.
98+
99+
[source,csharp]
100+
----
101+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=usings-serialization]
102+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=customer-with-jsonconverter-attribute]
103+
----
104+
<1> The `JsonConverter` attribute signals to `System.Text.Json` that it should use a converter of type `CustomerConverter` when serializing instances of this class.
105+
106+
When serializing this class, rather than include a string value representing the value of the `CustomerType` property, we must send a boolean property named `isStandard`. This requirement can be achieved with a custom JsonConverter implementation.
107+
108+
[source,csharp]
109+
----
110+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=converter-usings]
111+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=customer-converter]
112+
----
113+
<1> When reading, this converter will read the `isStandard` boolean and translate this to the correct `CustomerType` enum value.
114+
<2> When writing, this converter will translate the `CustomerType` enum value to an `isStandard` boolean property.
115+
116+
We can then index a customer document into {es}.
117+
118+
[source,csharp]
119+
----
120+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=index-customer-with-converter]
121+
----
122+
123+
The `Customer` instance is serialized using the custom converter, creating the following JSON document.
124+
125+
[source,javascript]
126+
----
127+
{
128+
"customerName": "Customer Ltd",
129+
"isStandard": false
130+
}
131+
----
132+
95133
[[injecting-custom-serializer]]
96134
===== Injecting a custom serializer
97135

tests/Tests/Documentation/ClientConcepts/Serialization/CustomSerializationTests.cs

+93-75
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010

1111
#pragma warning disable IDE0005
1212
//tag::usings[]
13+
//tag::converter-usings[]
1314
using System;
1415
using System.Text.Json;
1516
//tag::usings-serialization[]
1617
using System.Text.Json.Serialization;
1718
//end::usings-serialization[]
19+
//end::converter-usings[]
1820
using System.Threading.Tasks;
1921
using Elastic.Transport;
2022
using Elastic.Clients.Elasticsearch;
@@ -37,7 +39,8 @@ public async Task CustomizingJsonSerializerOptions()
3739

3840
#pragma warning disable format
3941
//tag::custom-options-local-function[]
40-
static void ConfigureOptions(JsonSerializerOptions o) => o.PropertyNamingPolicy = null; // <1>
42+
static void ConfigureOptions(JsonSerializerOptions o) => // <1>
43+
o.PropertyNamingPolicy = null;
4144
//end::custom-options-local-function[]
4245

4346
//tag::create-client[]
@@ -64,8 +67,7 @@ public async Task CustomizingJsonSerializerOptions()
6467
#pragma warning disable format
6568
//tag::index-person[]
6669
var person = new Person { FirstName = "Steve" };
67-
var indexResponse = await client.IndexAsync(person, "my-index-name");
68-
//end::index-person[]
70+
var indexResponse = await client.IndexAsync(person, "my-index-name"); //end::index-person[]
6971
#pragma warning restore format
7072

7173
var requestJson = Encoding.UTF8.GetString(indexResponse.ApiCallDetails.RequestBodyInBytes);
@@ -77,13 +79,13 @@ public async Task CustomizingJsonSerializerOptions()
7779

7880
// Alternative example using an Action
7981
//tag::custom-options-action[]
80-
Action<JsonSerializerOptions> configureOptions = o => o.PropertyNamingPolicy = null; // <3>
82+
Action<JsonSerializerOptions> configureOptions = o => o.PropertyNamingPolicy = null;
8183
//end::custom-options-action[]
8284
}
8385

8486
#pragma warning disable format
8587
//tag::person-class[]
86-
private class Person
88+
public class Person
8789
{
8890
public string FirstName { get; set; }
8991
}
@@ -115,7 +117,11 @@ public async Task UsingSystemTextJsonConverterAttributes()
115117
{
116118
#pragma warning disable format
117119
//tag::index-customer-with-converter[]
118-
var customer = new Customer { CustomerName = "Customer Ltd", Type = CustomerType.Enhanced };
120+
var customer = new Customer
121+
{
122+
CustomerName = "Customer Ltd",
123+
CustomerType = CustomerType.Enhanced
124+
};
119125
var indexResponse = await Client.IndexAsync(customer, "my-index-name");
120126
//end::index-customer-with-converter[]
121127
#pragma warning restore format
@@ -126,12 +132,12 @@ public async Task UsingSystemTextJsonConverterAttributes()
126132
var ms = new MemoryStream(indexResponse.ApiCallDetails.RequestBodyInBytes);
127133
var deserializedCustomer = Client.SourceSerializer.Deserialize<Customer>(ms);
128134
deserializedCustomer.CustomerName.Should().Be("Customer Ltd");
129-
deserializedCustomer.Type.Should().Be(CustomerType.Enhanced);
135+
deserializedCustomer.CustomerType.Should().Be(CustomerType.Enhanced);
130136
}
131137

132138
#pragma warning disable format
133139
//tag::person-class-with-attributes[]
134-
private class Person
140+
public class Person
135141
{
136142
[JsonPropertyName("forename")] // <1>
137143
public string FirstName { get; set; }
@@ -142,92 +148,103 @@ private class Person
142148
//end::person-class-with-attributes[]
143149
#pragma warning restore format
144150

145-
[JsonConverter(typeof(CustomerConverter))]
146-
private class Customer
147-
{
148-
public string CustomerName { get; set; }
149-
public CustomerType Type { get; set; }
150-
}
151+
#pragma warning disable format
152+
//tag::customer-with-jsonconverter-attribute[]
153+
[JsonConverter(typeof(CustomerConverter))] // <1>
154+
public class Customer
155+
{
156+
public string CustomerName { get; set; }
157+
public CustomerType CustomerType { get; set; }
158+
}
151159

152-
private enum CustomerType
153-
{
154-
Standard,
155-
Enhanced
156-
}
160+
public enum CustomerType
161+
{
162+
Standard,
163+
Enhanced
164+
}
165+
//end::customer-with-jsonconverter-attribute[]
166+
#pragma warning restore format
157167

158-
private class CustomerConverter : JsonConverter<Customer>
168+
#pragma warning disable format
169+
//tag::customer-converter[]
170+
public class CustomerConverter : JsonConverter<Customer>
171+
{
172+
public override Customer Read(ref Utf8JsonReader reader,
173+
Type typeToConvert, JsonSerializerOptions options)
159174
{
160-
public override Customer Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
161-
{
162-
var customer = new Customer();
175+
var customer = new Customer();
163176

164-
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
177+
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
178+
{
179+
if (reader.TokenType == JsonTokenType.PropertyName)
165180
{
166-
if (reader.TokenType == JsonTokenType.PropertyName)
181+
if (reader.ValueTextEquals("customerName"))
167182
{
168-
if (reader.ValueTextEquals("customerName"))
183+
reader.Read();
184+
customer.CustomerName = reader.GetString();
185+
continue;
186+
}
187+
188+
if (reader.ValueTextEquals("isStandard")) // <1>
189+
{
190+
reader.Read();
191+
var isStandard = reader.GetBoolean();
192+
193+
if (isStandard)
169194
{
170-
reader.Read();
171-
customer.CustomerName = reader.GetString();
172-
continue;
195+
customer.CustomerType = CustomerType.Standard;
173196
}
174-
175-
if (reader.ValueTextEquals("isStandard"))
197+
else
176198
{
177-
reader.Read();
178-
var isStandard = reader.GetBoolean();
179-
180-
if (isStandard)
181-
{
182-
customer.Type = CustomerType.Standard;
183-
}
184-
else
185-
{
186-
customer.Type = CustomerType.Enhanced;
187-
}
188-
189-
continue;
199+
customer.CustomerType = CustomerType.Enhanced;
190200
}
201+
202+
continue;
191203
}
192204
}
193-
194-
return customer;
195205
}
196206

197-
public override void Write(Utf8JsonWriter writer, Customer value, JsonSerializerOptions options)
207+
return customer;
208+
}
209+
210+
public override void Write(Utf8JsonWriter writer,
211+
Customer value, JsonSerializerOptions options)
212+
{
213+
if (value is null)
198214
{
199-
if (value is null)
200-
{
201-
writer.WriteNullValue();
202-
return;
203-
}
215+
writer.WriteNullValue();
216+
return;
217+
}
204218

205-
writer.WriteStartObject();
219+
writer.WriteStartObject();
206220

207-
if (!string.IsNullOrEmpty(value.CustomerName))
208-
{
209-
writer.WritePropertyName("customerName");
210-
writer.WriteStringValue(value.CustomerName);
211-
}
221+
if (!string.IsNullOrEmpty(value.CustomerName))
222+
{
223+
writer.WritePropertyName("customerName");
224+
writer.WriteStringValue(value.CustomerName);
225+
}
212226

213-
writer.WritePropertyName("isStandard");
227+
writer.WritePropertyName("isStandard");
214228

215-
if (value.Type == CustomerType.Standard)
216-
{
217-
writer.WriteBooleanValue(true);
218-
}
219-
else
220-
{
221-
writer.WriteBooleanValue(false);
222-
}
223-
224-
writer.WriteEndObject();
229+
if (value.CustomerType == CustomerType.Standard) // <2>
230+
{
231+
writer.WriteBooleanValue(true);
232+
}
233+
else
234+
{
235+
writer.WriteBooleanValue(false);
225236
}
237+
238+
writer.WriteEndObject();
226239
}
240+
}
241+
//end::customer-converter[]
242+
#pragma warning restore format
227243

228244
private class CustomerTypeConverter : JsonConverter<CustomerType>
229245
{
230-
public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
246+
public override CustomerType Read(ref Utf8JsonReader reader,
247+
Type typeToConvert, JsonSerializerOptions options)
231248
{
232249
return reader.GetString() switch
233250
{
@@ -238,7 +255,8 @@ public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert,
238255
};
239256
}
240257

241-
public override void Write(Utf8JsonWriter writer, CustomerType value, JsonSerializerOptions options)
258+
public override void Write(Utf8JsonWriter writer,
259+
CustomerType value, JsonSerializerOptions options)
242260
{
243261
switch (value)
244262
{
@@ -296,19 +314,19 @@ public async Task DerivingFromSystemTextJsonSerializer_ToRegisterACustomEnumConv
296314
deserializedCustomer.Type.Should().Be(CustomerType.Enhanced);
297315
}
298316

299-
private class Customer
317+
public class Customer
300318
{
301319
public string CustomerName { get; set; }
302320
public CustomerType Type { get; set; }
303321
}
304322

305-
private enum CustomerType
323+
public enum CustomerType
306324
{
307325
Standard,
308326
Enhanced
309327
}
310328

311-
private class MyCustomSerializer : SystemTextJsonSerializer
329+
public class MyCustomSerializer : SystemTextJsonSerializer
312330
{
313331
private readonly JsonSerializerOptions _options;
314332

@@ -324,7 +342,7 @@ public MyCustomSerializer(IElasticsearchClientSettings settings) : base(settings
324342
protected override JsonSerializerOptions CreateJsonSerializerOptions() => _options;
325343
}
326344

327-
private class CustomerTypeConverter : JsonConverter<CustomerType>
345+
public class CustomerTypeConverter : JsonConverter<CustomerType>
328346
{
329347
public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
330348
{

0 commit comments

Comments
 (0)