Search code examples
c#json.net.net-6.0system.text.jsonjsonconverter

How to implement a conditional custom Json Converter hide a property


I have a REST API in .Net that returns the following object:

public class MyDto 
{
  public string Name {get; set;}
  public int Age {get; set;}
  public string ContactNumber {get; set;}
}

I want to implement a custom JSON Converter that I can add as an attribute. When the object is serialized for the response, based on a value in an HTTP header, I want to exclude a specific property.

For example, if I add an attribute like this:

public class MyDto 
    {
      public string Name {get; set;}
      [JsonConverter(typeof(CustomConverter))]
      public int Age {get; set;}
      public string ContactNumber {get; set;}
    }

The response, when a particular header is set, should be:

{
 "Name":"Name",
 "ContactNumber":"1234567"
}

Solution

  • You need an other approach, since a converter doesn't have access to the HttpContext.

    Starting from System.Text.Json version 7.0.0 you can customize a JSON contract.
    This version 7.0.0 can be used from within a NET6 application.

    From the documentation:

    The System.Text.Json library constructs a JSON contract for each .NET type, which defines how the type should be serialized and deserialized.

    One topic describes what you want to achieve:

    Modification: Conditionally serialize a property
    How to achieve: Modify the JsonPropertyInfo.ShouldSerialize predicate for the property.


    Define a custom attribute that acts as a marker to target the properties that should be conditionally excluded.

    public class ConditionallyExcludedAttribute : Attribute
    { }
    
    public class MyDto 
    {
        public string Name { get; set; }
        
        [ConditionallyExcluded]    
        public int Age { get; set; }
        
        public string ContactNumber { get; set; }
    }
    

    Set up a custom DefaultJsonTypeInfoResolver that returns false for ShouldSerialize for the properties on your DTO models that have been marked with your custom attribute.

    public sealed class CustomJsonTypeInfoResolver : DefaultJsonTypeInfoResolver
    {
        public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
        {
            var typeInfo = base.GetTypeInfo(type, options);
    
            if (type.IsClass)
            {
                var propertiesToExclude = typeInfo.Properties.Where(o =>
                    o.AttributeProvider?.GetCustomAttributes(false)?.Any(o => o is ConditionallyExcludedAttribute) ?? false
                    ).ToList();
    
                propertiesToExclude.ForEach(o => o.ShouldSerialize = (_, _) => false);
    
                // Or remove the property.
                // propertiesToExclude.ForEach(o => typeInfo.Properties.Remove(o));
            }
    
            return typeInfo;
        }
    }
    

    Define a custom SystemTextJsonOutputFormatter that uses custom JsonSerializerOptions with this CustomJsonTypeInfoResolver installed.

    This custom formatter must only be applied when that expected HTTP header is present - see the CanWriteResult override.

    public class CustomJsonOutputFormatter : SystemTextJsonOutputFormatter
    {
        public CustomJsonOutputFormatter()
            : base(new JsonSerializerOptions()
            {
                TypeInfoResolver = new CustomJsonTypeInfoResolver()
                // Other settings go here
            })
        { }
    
        public override bool CanWriteResult(OutputFormatterCanWriteContext context)
        {
            var myHeader = context.HttpContext.Request.Headers["MyHeader"].ToString();
            return !string.IsNullOrEmpty(myHeader);
        }
    }
    

    Register this custom formatter in program.cs.
    Make sure that this CustomJsonOutputFormatter is at the top in the list of output formatters, at least before the default formatter, otherwise it doesn't get the chance to decide whether to take action (via CanWriteResult).

    builder.Services.AddControllers(
        o => o.OutputFormatters.Insert(0, new CustomJsonOutputFormatter())
        );
    

    When that specific HTTP header has a value the custom JSON serialization will take effect, otherwise the regular one.

    You can make all this more configurable and relying less on hard coded values, but that would make this question and answer to broad.