Phone numbers and email addresses are written as JSON strings; postal addresses are written as JSON objects.
dotnet add package RecursiveAI.OneContactRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
using System.Text.Json; using RecursiveAI.OneContact; var contact = new Contact( PhoneNumber.Parse("+1 202-555-0123"), EmailAddress.Parse("reader@EXAMPLE.COM"), PostalAddress.Parse( "123 Main St, Springfield IL 62704", Region.US)); var json = JsonSerializer.Serialize(contact); var restored = JsonSerializer.Deserialize<Contact>(json); Console.WriteLine(json); record Contact(PhoneNumber Phone, EmailAddress Email, PostalAddress Address);
{"Phone":"+12025550123","Email":"reader@example.com","Address":{"canonical":"123 MAIN ST, SPRINGFIELD IL 62704","text":"123 Main St, Springfield IL 62704","hint":"US","country":"US","adminArea":"IL","locality":"Springfield","dependentLocality":"","postalCode":"62704","sortingCode":"","organization":"","recipient":"","streetLines":["123 MAIN ST"],"flags":32,"sourceStart":0,"sourceLength":33}}Unzip the example, then dotnet run.
Full program
The contact below uses a fictional phone number, an example.com mailbox and a made-up postal address. The shipped converters run automatically for these value types.
using System.Text.Json; using RecursiveAI.OneContact; var contact = new Contact( PhoneNumber.Parse("+1 202-555-0123"), EmailAddress.Parse("reader@EXAMPLE.COM"), PostalAddress.Parse("123 Main St, Springfield IL 62704", Region.US)); string json = JsonSerializer.Serialize(contact); Console.WriteLine(json); var restored = JsonSerializer.Deserialize<Contact>(json)!; Console.WriteLine(JsonSerializer.Serialize(restored.Phone)); Console.WriteLine(JsonSerializer.Serialize(restored.Email)); Console.WriteLine(restored.Address.Canonical); record Contact(PhoneNumber Phone, EmailAddress Email, PostalAddress Address);
{"Phone":"+12025550123","Email":"reader@example.com","Address":{"canonical":"123 MAIN ST, SPRINGFIELD IL 62704","text":"123 Main St, Springfield IL 62704","hint":"US","country":"US","adminArea":"IL","locality":"Springfield","dependentLocality":"","postalCode":"62704","sortingCode":"","organization":"","recipient":"","streetLines":["123 MAIN ST"],"flags":32,"sourceStart":0,"sourceLength":33}} "+12025550123" "reader@example.com" 123 MAIN ST, SPRINGFIELD IL 62704
Rejected contact tokens throw JsonException with a parse reason and byte offset; the underlying parse exception is available as InnerException.
Variations
Read a national phone string with a region
using System.Text.Json; using RecursiveAI.OneContact; using RecursiveAI.OneContact.Serialization; var options = new JsonSerializerOptions(); options.Converters.Add(new PhoneNumberJsonConverter(Region.US)); var phone = JsonSerializer.Deserialize<PhoneNumber>( "\"202-555-0123\"", options); Console.WriteLine(phone);
+12025550123Inspect a rejected token
using System.Text.Json; using RecursiveAI.OneContact; try { JsonSerializer.Deserialize<PhoneNumber>("\"123\""); } catch (JsonException error) { Console.WriteLine(error.InnerException is ContactParseException); Console.WriteLine(error.Message); }
True The JSON value is not a valid PhoneNumber: a national number needs a region to be read in (phone.region-required) (at byte offset 0).
Related recipes
The short answer
Use System.Text.Json directly on OneContact’s typed values. PhoneNumber and EmailAddress serialize as strings; PostalAddress serializes as an object that carries fields and parsing context. Deserialize into the same contact type to restore those values. The converters are already attached to the types, so international phone numbers need no manual converter registration.
Use a regional converter only when input JSON contains national phone strings or free-text address strings whose country is known separately. A missing phone region is not a reason to guess one from the user’s interface language.
How do I serialize a contact object to JSON in C#?
The full program defines a record with phone, email and address properties, calls JsonSerializer.Serialize, then deserializes that JSON into the record type. Each contact property delegates its wire representation to its shipped converter. This differs from serializing the public properties of each value as an arbitrary object.
The documented JSON contract specifies canonical phone text. Email uses canonical text too, but includes a display name when the value has one. Postal addresses have a fixed camelCase shape, irrespective of your serializer naming policy. Its members include canonical, country, language when present, address fields, streetLines, flags, sourceStart and sourceLength.
For a parsed address, the object also stores text and the country hint when supplied. Reading that object parses the stored text again with its stored hint. An address made from fields reads back from its fields instead. The regional address converter affects plain string input; it does not override an object’s stored hint. See the converter implementation at this release.
Keep an original input property of your own if you need an exact audit copy. Canonical phone and email JSON is not a promise to reproduce the original spacing, punctuation or domain casing. Address source offsets describe stored source text; do not assume they point into some different input you have subsequently edited.
How do I deserialize JSON phone numbers in C#?
International phone strings work with the default attribute converter. National strings need an explicit region, shown in the first variation. Add PhoneNumberJsonConverter(Region.US) to the options before deserializing, and pass those options to the serializer call. Use the caller’s known country rather than applying one region indiscriminately to an international data set.
The second variation shows why a JSON string can be syntactically valid yet invalid as a phone value. The token "123" has no international prefix or country context. The converter refuses it with phone.region-required, and the sample prints both the message and whether its inner exception is a ContactParseException.
Treat JSON syntax errors and rejected contact content as import errors, not default contact values. Log the stable ReasonCode.Id from a contact parse exception when you need machine-readable categorization. Preserve a default branch when switching on reason values, because later releases can add reasons. For general serialization options and file writing, see the .NET serialization guide.