0

My question is similar to this one: How to ignore JsonProperty(PropertyName = "someName") when serializing json? of which the solution works for me, but I'm curious to know whether it is possible to extend the [JsonProperty] attribute with more properties (using Newtonsoft.json)?

Some background:
I have an application (let's call it SmartModel) that generates a physics model in c# based on user inputs. SmartModel consists of many classes with many properties (e.g. Pipe class with properties such as Length, Diameter etc.). SmartModel writes out a json type DTO to be used in a separate application for solving purposes (let's call it Solver and the DTO, SolverDTO). However, in addition to this SmartModel also writes out a different DTO for saving and opening purposes (conveniently called SmartModelDTO).

In this regard, it would be convenient to have a decorator above certain properties in SmartModel (e.g. [JsonProperty(SolverPropertyName = "someName")]) and then set up a contract resolver to serialize and write out (in json format):

  1. the SolverPropertyName when the SolverDTO is generated and
  2. the UnderlyingName when the SmartModelDTO is generated

(where UnderlyingName is already a property of JsonProperty by default and SolverPropertyName the property with which JsonProperty should be extended with).


EDIT

Herewith a minimal code sample to explain what I'm trying to achieve:

I have an example class called Pipe as follows:

class Pipe
{
    [JsonProperty(PropertyName = "z_axis_dimension")]
    public double Length { get; set; }

    [JsonProperty(PropertyName = "cross_sectional_dimension")]
    public double Diameter { get; set; }
}

I want to serialize in two different ways (in future maybe more). For the SmartModelDTO I would like the serializer to use the UnderlyingProperty, but for the SolverDTO, I would like to use the PropertyName in the JsonProperty attribute. To achieve this, the following contract resolver can be implemented:

class IgnoreJsonPropertyNameContractResolver : DefaultContractResolver
{
    protected override IList<JsonProperty> CreateProperties(
        Type type, 
        MemberSerialization memberSerialization)
    {
        IList<JsonProperty> list = base.CreateProperties(
            type, 
            memberSerialization);

        foreach (JsonProperty prop in list)
        {
            prop.PropertyName = prop.UnderlyingName;
        }

        return list;
    }
}

An example for how it is used as follows:

// Instance of Pipe:
Pipe pipe = new()
{
    Length = 10.2,
    Diameter = 5.5,
};

// Set some Json serializer settings:
JsonSerializerSettings jsonSerializerSettings = new();
jsonSerializerSettings.Formatting = Formatting.Indented;

// Serialize Pipe where property values are obtained from the
// JsonProperty PropertyName:
string solverJsonString = JsonConvert.SerializeObject(
    pipe, jsonSerializerSettings);
Console.WriteLine($"Serialized string for SolverDTO:");
Console.WriteLine($"{solverJsonString}");
Console.WriteLine();

// Set a new contract resolver to return the
// JsonProperty UnderlyingName instead of the PropertyName:
jsonSerializerSettings.ContractResolver =
    new IgnoreJsonPropertyNameContractResolver();

// Serialize Pipe where property values are obtained from the
// JsonProperty UnderlyingName:
string smartModelJsonString = JsonConvert.SerializeObject(
    pipe, jsonSerializerSettings);
Console.WriteLine($"Serialized string for SmartModelDTO:");
Console.WriteLine($"{smartModelJsonString}");

Console.ReadLine();

which gives the following output:

Serialized string for SolverDTO:
{
  "z_axis_dimension": 10.2,
  "cross_sectional_dimension": 5.5
}

Serialized string for SmartModelDTO:
{
  "Length": 10.2,
  "Diameter": 5.5
}

However, I would like to have a functionality in Pipe to label the properties for example as follows:

class Pipe
{
    [JsonProperty(
    SolverPropertyName = "z_axis_dimension", 
    APIPropertyName = "distance")]
    public double Length { get; set; }

    [JsonProperty(
    SolverPropertyName = "cross_sectional_dimension", 
    APIPropertyName = "diameter")]
    public double Diameter { get; set; }
}

and then set up different contract resolvers similar to the one above to serialize the Pipe object, but one serializes using SolverPropertyName, another one for APIPropertyName, etc...

Can the [JsonProperty] class be extended e.g. to have the properties SolverPropertyName, APIPropertyName, etc. in addition to PropertyName?

Francois Louw
  • 143
  • 2
  • 3
  • 13
  • Instead of describing your code, can you give us actual code with expected input/output? Preferably as small as possible, i.e. a [mre] – DavidG Oct 06 '21 at 20:32
  • @DavidG. Sure thing. I was explaining it in text as the post I referred to above also explained it in a similar way and got some answers. It is however fairly late where I am and I will post a minimal sample when its daylight again. Regards. – Francois Louw Oct 06 '21 at 20:37
  • @DavidG. I added a minimum reproducible example above. – Francois Louw Oct 07 '21 at 07:36

2 Answers2

1

I'm not sure if I get exactly want you want due to lack of code and an example (at the time of writing). But this will work to generate different serialised outputs from the same base.

Define an interface which has all your properties

public interface IBase
{
    string PropertyOne { get; set; }
    string PropertyTwo { get; set; }
    string PropertyThree { get; set; }
    string PropertyFour { get; set; }
}

Then your first class to be serialised which then uses the interface we defined. Here we use the JsonProperty attribute to define the output name that we want, which are different to the actual names of the properties.

We also use the ShouldSerialize feature of NewtonSoft to suppress outputting the fourth property which we don't want in this case.

public class First: IBase
{
    [JsonProperty("PropertyNumber1")]
    public string PropertyOne { get; set; }

    [JsonProperty("PropertyNumber2")]
    public string PropertyTwo { get; set; }

    [JsonProperty("PropertyNumber3")]
    public string PropertyThree { get; set; }

    public string PropertyFour { get; set; }
    public bool ShouldSerializePropertyFour() { return false; }
}

Then we define our other class similarly, but here we suppress the first property, and give the rest a different name via JsonProperty

public class Second : IBase
{
    [JsonProperty("SecondProperty")]
    public string PropertyTwo { get; set; }

    [JsonProperty("ThirdProperty")]
    public string PropertyThree { get; set; }

    [JsonProperty("FourthProperty")]
    public string PropertyFour { get; set; }

    public string PropertyOne { get; set; }
    public bool ShouldSerializePropertyOne() { return false; }
}

Then when we serialise the classes then we only get the properties we want, and they have different names.

Rather than using interfaces you can also do the same with an abstract class, but here you need to define the properties as virtual in the abstract class and override them in the child classes. Note that in order for the ShouldSerialize to work you will need to first override it and then suppress it!

jason.kaisersmith
  • 8,712
  • 3
  • 29
  • 51
  • @json.kaisersmith. Thanks for the reply. It is not entirely what I'm looking for, but I've added a minimal reproducible sample to the original post, which might clear up any misunderstanding. – Francois Louw Oct 07 '21 at 07:43
  • Thanks for this solution. Although not entirely what I was looking for in my question above (given the context I provide in the comment to the answer I selected for implementation), I there is an area in my code where I can implement it. I sometimes need to ignore certain properties when producing certain jsons from my model, for which it would be handy. Regards – Francois Louw Oct 08 '21 at 07:05
1

I will add this as a separate answer. I think the other answer I provided would still work for you and leads to more concise and easier to read code.

This method below will work, but it is (IMHO) messier (it could maybe be improved) and could lead to more mistakes.

The JsonProperty attribute from Newtonsoft is sealed, so we can't override this. This means we first need to create our own attribute. In here we define two properties which correspond to the names you want to use.

[AttributeUsage(AttributeTargets.Property)]
public class MyJsonPropertyAttribute : Attribute 
{
    public string SolverPropertyName { get; set; }
    public string APIPropertyName { get; set; }
}

Then you can decorate your class like this

public class Pipe
{
    [MyJsonProperty(
    SolverPropertyName = "z_axis_dimension",
    APIPropertyName = "distance")]
    public double Length { get; set; }

    [MyJsonProperty(
    SolverPropertyName = "cross_sectional_dimension",
    APIPropertyName = "diameter")]
    public double Diameter { get; set; }
}

Next we need to define our custom contract resolver which we will call DualNameContractResolver. During construction we need to specify the name of the attribute property we want to use. You could also change this to be an enum (which would lead to less mistakes). If you specify the wrong name then the default serialisation from Newtonsoft will be applied.

public class DualNameContractResolver : DefaultContractResolver
{
    private readonly string _propertyDecoratorToUse;

    public DualNameContractResolver(string propertyDecoratorToUse)
    {
        _propertyDecoratorToUse = propertyDecoratorToUse ?? throw new ArgumentNullException(nameof(propertyDecoratorToUse));
    }

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        CustomAttributeNamedArgument namedArgument = member.CustomAttributes.SelectMany(c => c.NamedArguments).Where(w => w.MemberName.Equals(_propertyDecoratorToUse)).FirstOrDefault();
        if(namedArgument.TypedValue.Value != null)
            property.PropertyName = namedArgument.TypedValue.Value.ToString();

        return property;
    }
}

As you see, in the CreateProperty method then we look for the requested property and if found apply the defined value (which is the serialisation name).

You can then use all of that like this.

Pipe pipe = new()
{
    Length = 10.2,
    Diameter = 5.5,
};

var solverResolver = new DualNameContractResolver("SolverPropertyName");                
var apiResolver = new DualNameContractResolver("APIPropertyName");    
JsonSerializerSettings jsonSerializerSettings = new();
jsonSerializerSettings.Formatting = Formatting.Indented;

jsonSerializerSettings.ContractResolver = solverResolver;
string json1 = JsonConvert.SerializeObject(pipe, jsonSerializerSettings);

jsonSerializerSettings.ContractResolver = apiResolver;
string json2 = JsonConvert.SerializeObject(pipe, jsonSerializerSettings);
jason.kaisersmith
  • 8,712
  • 3
  • 29
  • 51
  • This answer suits me very well. I mostly want all the properties of an object, just with different names for different serializations. Hence the solution below might lead to an excessive code base just for the purpose of different property names when serialization takes place. I know that decorators to properties clutters the code a bit. However, in my case I have many physical components (like pumps, pipes, heat exchangers, etc...). Some of them have more than 40 properties to complete their definition. "Duplicating" (and mapping) each of them would be excessive. – Francois Louw Oct 08 '21 at 07:29