-->

C# data contract for complex JSON object

2020-04-17 06:58发布

问题:

This is probably something really simple and I looked everywhere and tried everything I could come up with. So I apologize if this is a simple search and I was just looking for the wrong thing. I'm also new to data contracts and somewhat JSON so this probably isn't really that complex.

I am creating an API to ingest JSON and store it in our database. The JSON will look something like this:

{
"appname" : "MyApp",
"key" : "Test123",
"data" :[
  { "data1" : "10551296", "data2" : "TrainingIns", "data3" : "Completed"}
  ,
  { "connectorType" : "webserver-to-appserver", "sourceUri" : "data4", "destinationUri" : "data5", "rails" : "N", "data6" : "null" }
  ,
  { "groupId" : "group1", "failbackAction" : "null", "normal" : "null", "failoverAction" : "null", "failbackAction" : "null", "failoverAction" : "null", "artifactId" : "mywebserver", "normalState" : "null" }
  ,
  { "instanceId" : "10551296abc" }]
,
"updateId" : "MyID",
"updateTS" : "30-AUG-16 05.56.24.000000000 AM" ,
"creationUser" : "APICall"
}

Where the 'data' field will be an array with a variable amount of JSON objects. The issue I am having stems from either not getting data in the 'data' object or having it be completely undefined.

[DataContract]
public class Update_DB
{
    [DataMember(Name = "appname", IsRequired = true)]
    public string appname { get; set; }
    [DataMember]
    public string key { get; set; }

    [DataMember(Name="data",IsRequired = true)]
    public List<JsonValue> data { get; set; }

    [DataMember]
    public string updateId { get; set; }
    [DataMember]
    public string updateTS { get; set; }
    [DataMember]
    public string creationUser { get; set; }
}

I've gathered I might need a collection of some sort? I've tried everything I could find but I don't know how I should define the data member for 'data'. The above contract gives me empty arrays when I do this:

string x = JsonConvert.SerializeObject(collection.data);

I can get every other field I just need to turn the 'data' field into a string.

Hopefully that is enough info. Thanks in advance for any help!

回答1:

Under normal circumstances, you could define your data property as a List<Dictionary<string, string>>, like so:

    [DataMember(Name = "data", IsRequired = true)]
    public List<Dictionary<string, string>> data { get; set; }

Then you would be able to serialize and deserialize it successfully with Json.NET. Unfortunately, one of your data objects has duplicated keys:

  {
     "groupId":"group1",
     "failbackAction":"null",
     "normal":"null",
     "failoverAction":"null",
     "failbackAction":"null",
     "failoverAction":"null",
     "artifactId":"mywebserver",
     "normalState":"null"
  },

Using duplicated keys is not recommended by the JSON standard, which states:

When the names within an object are not unique, the behavior of software that receives such an object is unpredictable.

In addition, c# dictionaries of course do not support duplicated keys, and data contract serialization does not duplicated property names.

However, it is possible to read a JSON object with duplicated keys using Json.NET's JsonReader and create a custom JsonConverter to handle duplicated keys.

First, define the following class to replace JsonValue. JsonValue is a silverlight-specific class whose use has been deprecated in overall .Net:

[JsonConverter(typeof(JsonValueListConverter))]
public sealed class JsonValueList
{
    public JsonValueList()
    {
        this.Values = new List<KeyValuePair<string, string>>();
    }

    public List<KeyValuePair<string, string>> Values { get; private set; }
}

class JsonValueListConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(JsonValueList).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.Null)
            return null;
        var jsonValue = (existingValue as JsonValueList ?? new JsonValueList());
        if (reader.TokenType != JsonToken.StartObject)
            throw new JsonSerializationException("Invalid reader.TokenType " + reader.TokenType);
        while (reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonToken.Comment:
                    break;
                case JsonToken.PropertyName:
                    {
                        var key = reader.Value.ToString();
                        if (!reader.Read())
                            throw new JsonSerializationException(string.Format("Missing value at path: {0}", reader.Path));
                        var value = serializer.Deserialize<string>(reader);
                        jsonValue.Values.Add(new KeyValuePair<string, string>(key, value));
                    }
                    break;
                case JsonToken.EndObject:
                    return jsonValue;
                default:
                    throw new JsonSerializationException(string.Format("Unknown token {0} at path: {1} ", reader.TokenType, reader.Path));
            }
        }
        throw new JsonSerializationException(string.Format("Unclosed object at path: {0}", reader.Path));
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        var jsonValue = (JsonValueList)value;
        writer.WriteStartObject();
        foreach (var pair in jsonValue.Values)
        {
            writer.WritePropertyName(pair.Key);
            writer.WriteValue(pair.Value);
        }
        writer.WriteEndObject();
    }
}

Notice the use of [JsonConverter(typeof(JsonValueListConverter))]. This specifies the use of a custom converter when serializing and deserializing JsonValueList.

Next, define your Update_DB class as follows:

[DataContract]
public class Update_DB
{
    [DataMember(Name = "appname", IsRequired = true)]
    public string appname { get; set; }
    [DataMember]
    public string key { get; set; }

    [DataMember(Name = "data", IsRequired = true)]
    public List<JsonValueList> data { get; set; }

    [DataMember]
    public string updateId { get; set; }
    [DataMember]
    public string updateTS { get; set; }
    [DataMember]
    public string creationUser { get; set; }
}

Now you will be able to serialize and deserialize your JSON successfully. Sample fiddle.

Update

If you do not have duplicated keys, you can define your class as follows:

[DataContract]
public class Update_DB
{
    [DataMember(Name = "appname", IsRequired = true)]
    public string appname { get; set; }
    [DataMember]
    public string key { get; set; }

    [DataMember(Name = "data", IsRequired = true)]
    public List<Dictionary<string, string>> data { get; set; }

    [DataMember]
    public string updateId { get; set; }
    [DataMember]
    public string updateTS { get; set; }
    [DataMember]
    public string creationUser { get; set; }
}

And then the following:

var collection = new Update_DB
{
    data = new List<Dictionary<string, string>>
    {
        new Dictionary<string, string>
        {
            {"data1", "10551296"},
            {"data2", "TrainingIns"},
            {"data3", "Completed"},
        },
        new Dictionary<string, string>
        {
            {"connectorType", "webserver-to-appserver"},
            {"sourceUri", "data4"},
            {"destinationUri", "data5"},
        },
    },
};

string x = JsonConvert.SerializeObject(collection.data, Formatting.Indented);

Console.WriteLine(x);

Produces the output:

[
  {
    "data1": "10551296",
    "data2": "TrainingIns",
    "data3": "Completed"
  },
  {
    "connectorType": "webserver-to-appserver",
    "sourceUri": "data4",
    "destinationUri": "data5"
  }
]

Sample fiddle.



回答2:

Another option is to use the dynamic keyword. You could use a list of this type for data (per below).

[DataContract]
public class Update_DB
{
    [DataMember(Name = "appname", IsRequired = true)]
    public string appname { get; set; }
    [DataMember]
    public string key { get; set; }

    [DataMember(Name = "data", IsRequired = true)]
    public List<dynamic> data { get; set; }

    [DataMember]
    public string updateId { get; set; }
    [DataMember]
    public string updateTS { get; set; }
    [DataMember]
    public string creationUser { get; set; }
}

From there, you could use the object by deserializing with JSON.Net, and access into the dynamic data object (assuming you know something about the shape of this dynamic object). Something like below will work based on the input string from the original post.

Update_DB dataObj = JsonConvert.DeserializeObject<Update_DB>(objectJSON);
string test = dataObj.data[1].connectorType; //evaluates to "webserver-to-appserver"


回答3:

Use Json2CSharp.com to make sure you have everything correct:

 public class Datum
{
public string data1 { get; set; }
public string data2 { get; set; }
public string data3 { get; set; }
public string connectorType { get; set; }
public string sourceUri { get; set; }
public string destinationUri { get; set; }
public string rails { get; set; }
public string data6 { get; set; }
public string groupId { get; set; }
public string failbackAction { get; set; }
public string normal { get; set; }
public string failoverAction { get; set; }
public string artifactId { get; set; }
public string normalState { get; set; }
public string instanceId { get; set; }
}

public class RootObject
{
public string appname { get; set; }
public string key { get; set; }
public List<Datum> data { get; set; }
public string updateId { get; set; }
public string updateTS { get; set; }
public string creationUser { get; set; }
}