Home > OS >  Selectively exclude a property on one JSON serialization but not on another
Selectively exclude a property on one JSON serialization but not on another

Time:12-29

I have a POCO like this

public class Foo {
   public string PartitionKey => $"foo-{Bar}";
   public string Bar { get; set; }
}

I'm storing that POCO as serialized JSON in a database (Azure Cosmos DB to be specific) and making it available to clients via a ASP.NET WebApi.

When serializing that document for Cosmos DB, I need the PartitionKey, so I cannot exclude it totally by using [JsonIgnore]. But I do not want it included in my API response. So what is the easiest way to achieve this? Ideally I don't want to write my own JsonSerializer, but maybe using some custom attribute?

Expected outcomes:

For the database:

{
  "partitionKey": "foo-alice",
  "bar": "alice"
}

For the api response:

{
  "bar": "alice"
}

Using .NET7 and System.Text.Json

CodePudding user response:

You have a couple options to selectively exclude the PartitionKey property during serialization.

Firstly, since the property is read-only, you could set JsonSerializerOptions.IgnoreReadOnlyProperties = true:

var options = new JsonSerializerOptions
{
    IgnoreReadOnlyProperties = true,
    // Add other options as required
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 
    WriteIndented = true,           
};

var json = JsonSerializer.Serialize(foo, options);

This will exclude all read-only properties including PartitionKey.

Secondly, if you need to exclude only PartitionKey but include other read-only properties, or exclude modifiable properties, or otherwise need more control over what gets serialized, you can add a DefaultJsonTypeInfoResolver modifier to exclude the unwanted members.

To exclude the property by name, add the following extension method:

public static partial class JsonSerializerExtensions
{
    public static DefaultJsonTypeInfoResolver Exclude(this DefaultJsonTypeInfoResolver resolver, Type type, params string [] membersToExclude)
    {
        if (resolver == null || membersToExclude == null)
            throw new ArgumentNullException();
        var membersToExcludeSet = membersToExclude.ToHashSet();
        resolver.Modifiers.Add(typeInfo => 
                               {
                                   if (typeInfo.Kind == JsonTypeInfoKind.Object && type.IsAssignableFrom(typeInfo.Type)) // Or type == typeInfo.Type if you don't want to exclude from subtypes
                                       foreach (var property in typeInfo.Properties)
                                           if (property.GetMemberName() is {} name && membersToExcludeSet.Contains(name))
                                               property.ShouldSerialize = static (obj, value) => false;
                               });
        return resolver;
    }

    public static string? GetMemberName(this JsonPropertyInfo property) => (property.AttributeProvider as MemberInfo)?.Name;
}

And now you will be able to do:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        .Exclude(typeof(Foo), nameof(Foo.PartitionKey)),
    // Add other options as required
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 
    WriteIndented = true,           
};

var json = JsonSerializer.Serialize(foo, options);

If you want to exclude by custom attribute rather than by name, introduce the following extension method:

public static partial class JsonSerializerExtensions
{
    public static DefaultJsonTypeInfoResolver ExcludeByAttribute<TAttribute>(this DefaultJsonTypeInfoResolver resolver) where TAttribute : System.Attribute
    {
        if (resolver == null)
            throw new ArgumentNullException();
        var attr = typeof(TAttribute);
        resolver.Modifiers.Add(typeInfo => 
                               {
                                   if (typeInfo.Kind == JsonTypeInfoKind.Object)
                                       foreach (var property in typeInfo.Properties)
                                           if (property.AttributeProvider?.IsDefined(attr, true) == true)
                                               property.ShouldSerialize = static (obj, value) => false;
                               });
        return resolver;
    }
}

Then modify Foo as follows:

[System.AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class JsonExludeFromResponseAttribute : System.Attribute { }

public class Foo {
    [JsonExludeFromResponseAttribute]
    public string PartitionKey => $"foo-{Bar}";
    public string Bar { get; set; }
}

And exclude PartitionKey as follows:

var options = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver()
        .ExcludeByAttribute<JsonExludeFromResponseAttribute>(),
    // Add other options as required
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 
    WriteIndented = true,           
};

var json = JsonSerializer.Serialize(foo, options);

Notes:

  • Contract customization is new in .NET 7.

  • If you have many different types with a PartitionKey property which you always want to exclude, you could pass in typeof(object) as the base class from which to exclude the property.

Demo fiddle here.

  • Related