Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Serialize Dynamic Property Name for an Object using JSON.NET

I'm using JSON.NET for serialization of my objects for connecting to a REST API. One of the properties in my object that needs to be serialized to JSON has a dynamic property name. If the value contained in the struct for this property is a numeric value, then the JSON property is "type_id", however if this value is a string value, then the JSON property name is "type_code". I attempted to use a custom JsonConverter for this, but I get a JsonWriterException with this message when I attempt to serialize:

"Token PropertyName in state Property would result in an invalid JSON object. Path ''."

Below is a subset of my object, as seen below I didn't specify a property name in my object for that as such:

[JsonProperty("title",Required=Required.Always,Order=1)]
public string Title { get; set; }

[JsonProperty("date",Order=3)]
[JsonConverter(typeof(IsoDateTimeConverter))]
public DateTime Date { get; set; }

[JsonProperty(Order=2)]
[JsonConverter(typeof(TypeIdentifierJsonConverter))]
public TypeIdentifier DocTypeIdentifier { get; set; }

In the TypeIdentifier class I have the following in my WriteJson() method:

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
    TypeIdentifier docTypeId;
    id= (TypeIdentifier) value;

    writer.WritePropertyName(id.ParameterName);
    writer.WriteValue(id.Value);           
}

However, I am assuming it's defaulting to the name of the object's property instead of my custom one, causing two property names for a single value within the JSON string. How can the property name be set dynamically for this, since the JsonPropertyAttribute tag appears to pull the object's property name when not specified explicitly?

NOTE: This object will never need to be deserialized from this app.

EDIT: This object is tagged with the [JsonObject(MemberSerialization.OptIn)] attribute

like image 952
JNYRanger Avatar asked Mar 28 '14 16:03

JNYRanger


People also ask

How do you serialize and deserialize an object in C# using JSON?

It returns JSON data in string format. In Deserialization, it does the opposite of Serialization which means it converts JSON string to custom . Net object. In the following code, it calls the static method DeserializeObject() of the JsonConvert class by passing JSON data.

What is Jsonproperty annotation C#?

JsonPropertyAttribute indicates that a property should be serialized when member serialization is set to opt-in. It includes non-public properties in serialization and deserialization. It can be used to customize type name, reference, null, and default value handling for the property value.


1 Answers

A JsonConverter cannot set the name of a property in a parent object. When the converter's WriteJson method is called, the property name has already been written to the JSON; the writer is expecting only a value that point. That is why you are getting an error. In order to make this work, the custom converter would have to be made for the parent object. That converter would then be responsible for writing the property names and values of its children.

Follow-up

It is possible to write a converter for the parent object such that the JSON attributes applied to it are still respected, while still achieving the result you want. I'll outline the approach below.

First, a little bit of setup. Since you did not say what your class was called, I'll assume for this example that it is called Document. We only need to make one substantive change to it, and that is to remove the [JsonConverter] attribute from the DocTypeIdentifier property. So we have:

[JsonObject(MemberSerialization.OptIn)]
class Document
{
    [JsonProperty("title", Required = Required.Always, Order = 1)]
    public string Title { get; set; }

    [JsonProperty("date", Order = 3)]
    [JsonConverter(typeof(IsoDateTimeConverter))]
    public DateTime Date { get; set; }

    [JsonProperty(Order = 2)]
    public TypeIdentifier DocTypeIdentifier { get; set; }

    public string OtherStuff { get; set; }
}

You also did not show the code for the TypeIdentifier class, so I'll just assume it looks like this, for sake of example:

class TypeIdentifier
{
    public string Value { get; set; }
    public string ParameterName { get; set; }
}

With that out of the way, we can make the converter. The approach is fairly straightforward: we load the Document into a JObject, taking advantage of the fact that it respects the attributes applied, then go back and fix the serialization of the DocTypeIdentifier since it needs special handling. Once we have that, we write out the JObject to the JsonWriter. Here is the code:

class DocumentConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return (objectType == typeof(Document));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Document doc = (Document)value;

        // Create a JObject from the document, respecting existing JSON attribs
        JObject jo = JObject.FromObject(value);

        // At this point the DocTypeIdentifier is not serialized correctly.
        // Fix it by replacing the property with the correct name and value.
        JProperty prop = jo.Children<JProperty>()
                           .Where(p => p.Name == "DocTypeIdentifier")
                           .First();

        prop.AddAfterSelf(new JProperty(doc.DocTypeIdentifier.ParameterName, 
                                        doc.DocTypeIdentifier.Value));
        prop.Remove();

        // Write out the JSON
        jo.WriteTo(writer);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

Now we have the converter, but the catch is we cannot simply decorate the Document class with a [JsonConverter] attribute in order to use it. If we did, we would end up with a recursive loop as the converter tried to use itself when we loaded the document into the JObject. So instead, we need to create an instance of the converter and pass it to the serializer via settings. The converter's CanConvert method ensures it gets used on the correct class. The JObject.FromObject method uses a different serializer instance internally, so it does not see the DocumentConverter and thus does not get into trouble.

JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new DocumentConverter());

string json = JsonConvert.SerializeObject(doc, settings);

Here is a demo showing the converter in action:

class Program
{
    static void Main(string[] args)
    {
        Document doc = new Document
        {
            Title = "How to write a JSON converter",
            Date = DateTime.Today,
            DocTypeIdentifier = new TypeIdentifier
            {
                ParameterName = "type_id",
                Value = "26"
            },
            OtherStuff = "this should not appear in the JSON"
        };

        JsonSerializerSettings settings = new JsonSerializerSettings();
        settings.Converters.Add(new DocumentConverter());
        settings.Formatting = Formatting.Indented;

        string json = JsonConvert.SerializeObject(doc, settings);
        Console.WriteLine(json);
    }
}

Here is the output from the above:

{
  "title": "How to write a JSON converter",
  "type_id": "26",
  "date": "2014-03-28T00:00:00-05:00"
}
like image 187
Brian Rogers Avatar answered Oct 25 '22 03:10

Brian Rogers