Skip to content

Commit 1ccb069

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

File tree

2 files changed

+141
-85
lines changed

2 files changed

+141
-85
lines changed

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

+49-12
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
----
@@ -70,8 +65,7 @@ private async Task SerializeWithCustomOptionsAsync()
7065
{
7166
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=custom-options-local-function]
7267
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=create-client]
73-
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=index-person]
74-
}
68+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=index-person]}
7569
----
7670
<1> A local function can be defined, accepting a `JsonSerializerOptions` parameter. Here, we set `PropertyNamingPolicy` to `null`. This returns to the default behavior for `System.Text.Json`, which uses Pascal Case.
7771
<2> When creating the `ElasticsearchClientSettings`, we supply a `SourceSerializerFactory` using a lambda. The factory function creates a new instance of `DefaultSourceSerializer`, passing in the `settings` and our `ConfigureOptions` local function. We have now configured the settings with a custom instance of the source serializer.
@@ -92,6 +86,49 @@ As an alternative to using a local function, we could store an `Action<JsonSeria
9286
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=custom-options-action]
9387
----
9488

89+
[[registering-custom-converters]]
90+
===== Registering custom `System.Text.Json` converters
91+
92+
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.
93+
94+
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.
95+
96+
Our class is defined, and the `JsonConverter` attribute is applied to the class type, specifying the type of a custom converter.
97+
98+
[source,csharp]
99+
----
100+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=usings-serialization]
101+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=customer-with-jsonconverter-attribute]
102+
----
103+
<1> The `JsonConverter` attribute signals to `System.Text.Json` that it should use a converter of type `CustomerConverter` when serializing instances of this class.
104+
105+
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.
106+
107+
[source,csharp]
108+
----
109+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=converter-usings]
110+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=customer-converter]
111+
----
112+
<1> When reading, this converter will read the `isStandard` boolean and translate this to the correct `CustomerType` enum value.
113+
<2> When writing, this converter will translate the `CustomerType` enum value to an `isStandard` boolean property.
114+
115+
We can then index a customer document into {es}.
116+
117+
[source,csharp]
118+
----
119+
include::{doc-tests-src}/ClientConcepts/Serialization/CustomSerializationTests.cs[tag=index-customer-with-converter]
120+
----
121+
122+
The `Customer` instance is serialized using the custom converter, creating the following JSON document.
123+
124+
[source,javascript]
125+
----
126+
{
127+
"customerName": "Customer Ltd",
128+
"isStandard": false
129+
}
130+
----
131+
95132
[[injecting-custom-serializer]]
96133
===== Injecting a custom serializer
97134

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

+92-73
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[]
@@ -77,13 +80,13 @@ public async Task CustomizingJsonSerializerOptions()
7780

7881
// Alternative example using an Action
7982
//tag::custom-options-action[]
80-
Action<JsonSerializerOptions> configureOptions = o => o.PropertyNamingPolicy = null; // <3>
83+
Action<JsonSerializerOptions> configureOptions = o => o.PropertyNamingPolicy = null;
8184
//end::custom-options-action[]
8285
}
8386

8487
#pragma warning disable format
8588
//tag::person-class[]
86-
private class Person
89+
public class Person
8790
{
8891
public string FirstName { get; set; }
8992
}
@@ -115,7 +118,11 @@ public async Task UsingSystemTextJsonConverterAttributes()
115118
{
116119
#pragma warning disable format
117120
//tag::index-customer-with-converter[]
118-
var customer = new Customer { CustomerName = "Customer Ltd", Type = CustomerType.Enhanced };
121+
var customer = new Customer
122+
{
123+
CustomerName = "Customer Ltd",
124+
CustomerType = CustomerType.Enhanced
125+
};
119126
var indexResponse = await Client.IndexAsync(customer, "my-index-name");
120127
//end::index-customer-with-converter[]
121128
#pragma warning restore format
@@ -126,12 +133,12 @@ public async Task UsingSystemTextJsonConverterAttributes()
126133
var ms = new MemoryStream(indexResponse.ApiCallDetails.RequestBodyInBytes);
127134
var deserializedCustomer = Client.SourceSerializer.Deserialize<Customer>(ms);
128135
deserializedCustomer.CustomerName.Should().Be("Customer Ltd");
129-
deserializedCustomer.Type.Should().Be(CustomerType.Enhanced);
136+
deserializedCustomer.CustomerType.Should().Be(CustomerType.Enhanced);
130137
}
131138

132139
#pragma warning disable format
133140
//tag::person-class-with-attributes[]
134-
private class Person
141+
public class Person
135142
{
136143
[JsonPropertyName("forename")] // <1>
137144
public string FirstName { get; set; }
@@ -142,92 +149,103 @@ private class Person
142149
//end::person-class-with-attributes[]
143150
#pragma warning restore format
144151

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

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

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

164-
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
178+
while (reader.Read() && reader.TokenType != JsonTokenType.EndObject)
179+
{
180+
if (reader.TokenType == JsonTokenType.PropertyName)
165181
{
166-
if (reader.TokenType == JsonTokenType.PropertyName)
182+
if (reader.ValueTextEquals("customerName"))
167183
{
168-
if (reader.ValueTextEquals("customerName"))
184+
reader.Read();
185+
customer.CustomerName = reader.GetString();
186+
continue;
187+
}
188+
189+
if (reader.ValueTextEquals("isStandard")) // <1>
190+
{
191+
reader.Read();
192+
var isStandard = reader.GetBoolean();
193+
194+
if (isStandard)
169195
{
170-
reader.Read();
171-
customer.CustomerName = reader.GetString();
172-
continue;
196+
customer.CustomerType = CustomerType.Standard;
173197
}
174-
175-
if (reader.ValueTextEquals("isStandard"))
198+
else
176199
{
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;
200+
customer.CustomerType = CustomerType.Enhanced;
190201
}
202+
203+
continue;
191204
}
192205
}
193-
194-
return customer;
195206
}
196207

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

205-
writer.WriteStartObject();
220+
writer.WriteStartObject();
206221

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

213-
writer.WritePropertyName("isStandard");
228+
writer.WritePropertyName("isStandard");
214229

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

228245
private class CustomerTypeConverter : JsonConverter<CustomerType>
229246
{
230-
public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
247+
public override CustomerType Read(ref Utf8JsonReader reader,
248+
Type typeToConvert, JsonSerializerOptions options)
231249
{
232250
return reader.GetString() switch
233251
{
@@ -238,7 +256,8 @@ public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert,
238256
};
239257
}
240258

241-
public override void Write(Utf8JsonWriter writer, CustomerType value, JsonSerializerOptions options)
259+
public override void Write(Utf8JsonWriter writer,
260+
CustomerType value, JsonSerializerOptions options)
242261
{
243262
switch (value)
244263
{
@@ -296,19 +315,19 @@ public async Task DerivingFromSystemTextJsonSerializer_ToRegisterACustomEnumConv
296315
deserializedCustomer.Type.Should().Be(CustomerType.Enhanced);
297316
}
298317

299-
private class Customer
318+
public class Customer
300319
{
301320
public string CustomerName { get; set; }
302321
public CustomerType Type { get; set; }
303322
}
304323

305-
private enum CustomerType
324+
public enum CustomerType
306325
{
307326
Standard,
308327
Enhanced
309328
}
310329

311-
private class MyCustomSerializer : SystemTextJsonSerializer
330+
public class MyCustomSerializer : SystemTextJsonSerializer
312331
{
313332
private readonly JsonSerializerOptions _options;
314333

@@ -324,7 +343,7 @@ public MyCustomSerializer(IElasticsearchClientSettings settings) : base(settings
324343
protected override JsonSerializerOptions CreateJsonSerializerOptions() => _options;
325344
}
326345

327-
private class CustomerTypeConverter : JsonConverter<CustomerType>
346+
public class CustomerTypeConverter : JsonConverter<CustomerType>
328347
{
329348
public override CustomerType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
330349
{

0 commit comments

Comments
 (0)