问题
I have a situation where an API has multiple array-like objects as individual properties on a object. For example:
"parent": {
id: 4,
/*... other fields ...*/
"prop_1": "A",
"prop_2": "B",
/*... other "props" ...*/
"prop_24": "W"
}
I want the resulting model in C# to not repeat that same structure and have prop_X deserialized as a List and serialized back to that mess.
class Parent {
[JsonProperty("id")]
public int ParentId { get; set; }
/*... other properties ...*/
public List<string> Props { get; set; }
}
I tried added JsonConverter
attribute to the Props
property, but I couldn't figure out how to get the props I needed on the parent. I could add a converter to the parent object, but for two reasons it causes a problem.
- Most of the fields map to simple properties and I don't want to have to write the code to manually deserialize and serialize all of them.
- The convention of "prop_xx" fields appears in multiple objects and I'd hate to write JsonConverters for each object.
My idea was to have all the objects implement an interface, IHasProps
, and write an IHasPropsJsonConverter
. The converter would try to use as much as the built in functionality to read and write props, except when encountering an attribute on a type that indicates its a prop object when write, and a field that matches the pattern ^prop\d+$
when reading.
This seems like overkill. Is there a better way?
回答1:
Your approach of using a converter should work but is a little tricky to do in a generic fashion without getting a stack overflow exception. For writing, see Generic method of modifying JSON before being returned to client for one way of doing it. For reading, you can load into a JObject, populate the regular properties along the lines of Json.NET custom serialization with JsonConverter - how to get the “default” behavior then identify and parse the "prop_XXX"
properties. Note that these solutions don't play well with TypeNameHandling or PreserveReferencesHandling.
However, a simpler approach may be to make use of [JsonExtensionData] to temporarily store your variable set of properties in a IDictionary<string, JToken>
during the serialization process and then add them to the List<string> Props
when serialization is complete. This can be done using serialization callbacks:
public class Parent
{
public Parent() { this.Props = new List<string>(); }
[JsonProperty("id")]
public int ParentId { get; set; }
[JsonProperty("value")]
public string Value { get; set; }
[JsonIgnore]
public List<string> Props { get; set; }
[JsonExtensionData]
JObject extensionData; // JObject implements IDictionary<string, JToken> and preserves document order.
[OnSerializing]
void OnSerializing(StreamingContext ctx)
{
VariablePropertyListExtensions.OnSerializing(Props, ref extensionData, false);
}
[OnSerialized]
void OnSerialized(StreamingContext ctx)
{
VariablePropertyListExtensions.OnSerialized(Props, ref extensionData, false);
}
[OnDeserializing]
void OnDeserializing(StreamingContext ctx)
{
VariablePropertyListExtensions.OnDeserializing(Props, ref extensionData, false);
}
[OnDeserialized]
void OnDeserialized(StreamingContext ctx)
{
if (Props == null)
Props = new List<string>();
VariablePropertyListExtensions.OnDeserialized(Props, ref extensionData, false);
}
}
public static class VariablePropertyListExtensions
{
public const string Prefix = "prop_";
readonly static Regex regex;
static VariablePropertyListExtensions()
{
regex = new Regex("^" + Prefix + @"\d+" + "$", RegexOptions.CultureInvariant | RegexOptions.Compiled); // Add | RegexOptions.IgnoreCase if required
}
public static void OnSerializing<TDictionary>(IList<string> props, ref TDictionary extensionData, bool keepUnknownProperties) where TDictionary : class, IDictionary<string, JToken>, new()
{
Debug.Assert(keepUnknownProperties || (extensionData == null || extensionData.Count == 0));
// Add the prop_ properties.
if (props == null || props.Count < 1)
return;
extensionData = extensionData ?? new TDictionary();
for (int i = 0; i < props.Count; i++)
extensionData.Add(Prefix + (i + 1).ToString(NumberFormatInfo.InvariantInfo), (JValue)props[i]);
}
internal static void OnSerialized<TDictionary>(IList<string> props, ref TDictionary extensionData, bool keepUnknownProperties) where TDictionary : class, IDictionary<string, JToken>, new()
{
// Remove the prop_ properties.
if (extensionData == null)
return;
foreach (var name in extensionData.Keys.Where(k => regex.IsMatch(k)).ToList())
extensionData.Remove(name);
// null out extension data if no longer needed
if (!keepUnknownProperties || extensionData.Count == 0)
extensionData = null;
}
internal static void OnDeserializing<TDictionary>(IList<string> props, ref TDictionary extensionData, bool keepUnknownProperties) where TDictionary : class, IDictionary<string, JToken>, new()
{
Debug.Assert(keepUnknownProperties || (extensionData == null || extensionData.Count == 0));
}
internal static void OnDeserialized<TDictionary>(IList<string> props, ref TDictionary extensionData, bool keepUnknownProperties) where TDictionary : class, IDictionary<string, JToken>, new()
{
props.Clear();
if (extensionData == null)
return;
foreach (var item in extensionData.Where(i => regex.IsMatch(i.Key)).ToList())
{
props.Add(item.Value.ToObject<string>());
extensionData.Remove(item.Key);
}
// null out extension data if no longer needed
if (!keepUnknownProperties || extensionData.Count == 0)
extensionData = null;
}
}
Here I have moved the logic for populating and deserializing the extension data dictionary to a helper class for reuse in multiple classes. Note that I am adding the "prop_XXX"
properties to the properties list in document order. Since the standard states that a JSON object is an unordered set of key/value pairs, for added robustness you might want to sort them by their XXX
index.
Sample fiddle.
来源:https://stackoverflow.com/questions/39251135/jsonconverter-where-the-property-is-composed-of-fields-on-its-parent