0

Is it possible to write a custom converter to serialize based on the name of the property?

An example is to keep the time part of a DateTime type property if the property name ends with "DateTime", otherwise discard the time part. The converter should work for all types and only target DateTime properties.

Doesn't seem like you can inspect the property name in the Write method of JsonConverter<T>.

dbc
  • 104,963
  • 20
  • 228
  • 340
zyq
  • 41
  • 9

1 Answers1

0

Yes, you can do that with reflection.

Let's suppose you have the following data model:

class Example
{
    public Guid Id { get; set; }
    public int InternalId { get; set; }
    public DateTime DateOnly { get; set; }
    public DateTime DateTime { get; set; }
}
  • As you can see we have two DateTime properties
    • One of them ends with Time
  • We also have two other properties just to make sure that we are not loosing type information during serialization

You can create a JsonConverter against the whole object like this:

class ExampleConverter : JsonConverter<Example>
{
    public override Example Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, Example value, JsonSerializerOptions options)
    {
        var objectProperties = value.GetType().GetProperties();
        var objectFieldNameValuePairs = new Dictionary<string, object>();
        foreach (var objectProperty in objectProperties)
        {
            if (objectProperty.PropertyType == typeof(DateTime))
            {
                var datetimeFieldValue = (DateTime)objectProperty.GetValue(value);
                var transformedValue = datetimeFieldValue.ToString(objectProperty.Name.EndsWith("Time") ? "g" : "d");
                objectFieldNameValuePairs.Add(objectProperty.Name, transformedValue);
            }
            else
                objectFieldNameValuePairs.Add(objectProperty.Name, objectProperty.GetValue(value));
        }

        writer.WriteStartObject();

        foreach (KeyValuePair<string,object> fieldNameAndValue in objectFieldNameValuePairs)
        {
            writer.WritePropertyName(fieldNameAndValue.Key);
            JsonSerializer.Serialize(writer, fieldNameAndValue.Value, options);
        }

        writer.WriteEndObject();
    }
}
  • First we get all properties of the Example
  • Then we iterate through them
  • If the property holds a DateTime then based on your provided heuristic we are using different date formatting string
    • Otherwise we don't do any conversion
  • Finally we serialize the Dictionary manually

Usage

var example = new Example { DateOnly = DateTime.UtcNow, DateTime = DateTime.UtcNow, Id = Guid.NewGuid(), InternalId = 100 };
var serializedExample = JsonSerializer.Serialize(example, new JsonSerializerOptions { Converters = { new ExampleConverter() } });

Console.WriteLine(serializedExample);

Output

{
   "Id":"eddd0620-b27c-4041-bd28-af6bfbd70583",
   "InternalId":100,
   "DateOnly":"7/23/2021",
   "DateTime":"7/23/2021 7:10 AM"
}

UPDATE Make the converter more generic

As it was discussed in the comments section the converter should not be tied to a specific class. It should be able to handle any class. Unfortunately we can't specify object as the type parameter of the JsonConverter.

On the other hand we can receive the type parameter when we create an instance from our converter. So, here is the revised converter code:

class DateTimeConverter<T> : JsonConverter<T>
{
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        throw new NotImplementedException();
    }

    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        PropertyInfo[] props = value.GetType().GetProperties();
        Dictionary<string, object> data = new Dictionary<string, object>();
        foreach (var prop in props)
        {
            if (prop.PropertyType == typeof(DateTime))
            {
                var date = (DateTime)prop.GetValue(value);
                data.Add(prop.Name, date.ToString(prop.Name.EndsWith("Time") ? "g" : "d"));
            }
            else
                data.Add(prop.Name, prop.GetValue(value));
        }

        writer.WriteStartObject();

        foreach (KeyValuePair<string,object> item in data)
        {
            writer.WritePropertyName(item.Key);
            JsonSerializer.Serialize(writer, item.Value, options);
        }

        writer.WriteEndObject();
    }
}

Usage

var ex = new Example
{
    DateOnly = DateTime.UtcNow,
    DateTime = DateTime.UtcNow,
    Id = Guid.NewGuid(),
    InternalId = 100
};
var aex = new AnotherExample
{
    CreationDate = DateTime.UtcNow,
    Description = "Testing"
};
var options = new JsonSerializerOptions
{
    Converters =
    {
        new DateTimeConverter<Example>(),
        new DateTimeConverter<AnotherExample>()
    }
};

Console.WriteLine(JsonSerializer.Serialize(ex, options));
Console.WriteLine(JsonSerializer.Serialize(aex, options));

Known limitation: This converter can't be used against anonymous types.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • thanks for your answer, I edited my question to clarify. Your solution would work for a particular type "Example", however I wanted a solution that works for all types. I guess I was looking for something like JsonConverter. Changing the generic argument to "Object" would work but that would stop me from using other custom converters? – zyq Jul 25 '21 at 05:57
  • @zyq The serialization logic is generic enough to handle any kind of Object. So, if you change the JsonConverter's T type to Object that will work. You can registers multiple converters not just a single one. The Options defines a Converters collection. – Peter Csala Jul 25 '21 at 06:54
  • Actually I just tried your code by changing `JsonConverter` to `JsonConverter`, the converter is not getting called. – zyq Jul 25 '21 at 09:25
  • @zyq Yes, you are right it does not work. I've found an alternative solution so, I've updated my post, please check it. – Peter Csala Jul 26 '21 at 06:54
  • I want the Datetime properties of all my types to be converted in this way. I can tweak this to use factory pattern converter so that I don't need to manually register all types however the issue is that by using this approach this converter will be used for all types. I won't be able to use other converters that target a custom type. – zyq Jul 27 '21 at 10:54
  • @zyq Yes, that's true. But unfortunately the `Write` receives only just the value not its "container". That's why a single DateTimeConverter can't solve your problem. But using two (like `DateConverter` and `DateAndTimeConverter`) might solve it. But you have to decorate each property explicitly with one or the other converter. So, I'm unaware of any solution which might not expose any trade-off(s). Hopefully someone else, who is smarter than me, will share some better solution. – Peter Csala Jul 27 '21 at 11:05
  • @zyq I had a new idea: Maybe we should forget the `JsonConverter` based solutions. If you want to have this behaviour in an ASP.NET Core application then you could [manipulate the response in a middleware](https://stackoverflow.com/questions/67554325/append-custom-html-output-before-and-after-response-in-asp-net-core/67689969#67689969). That gives you the ability to register any kind of converter and manipulate the response after serialization. The downside: you will apply the truncation for all response objects. If this solution is suitable for your needs I'm happy to leave another post. – Peter Csala Jul 28 '21 at 07:11