0

I have a case where I'm trying to set up an object with pre-defined default values before incorporating existing json data (using JSON.net). In some cases it works, though I think this is entirely because of empty arrays. The one case where it doesn't, I'm using a base snippet of json for one of the properties.

To set the default values, I implemented the answer I from How to pass DefaultValue for custom class when deserializing with Json.net for my custom classes.

[In LINQPad]

void Main()
{
  var appData = new AppData().Dump();
  var defaultSettings = @"{ }";
  appData = JsonConvert.DeserializeObject<AppData>(defaultSettings).Dump();

  var data = @"{
    ""Ranges"": [
      {
        ""Name"": ""Foo"",
        ""Minimum"": 110,
        ""Maximum"": 120,
        ""Include"": true
      },
      {
        ""Name"": ""Bar"",
        ""Minimum"": 125,
        ""Maximum"": 145,
        ""Include"": true
      }
    ]
  }";
  JsonConvert.PopulateObject(data, appData, new JsonSerializerSettings() { ObjectCreationHandling = ObjectCreationHandling.Replace });
  appData.Dump();
  var output = JsonConvert.SerializeObject(appData, Newtonsoft.Json.Formatting.Indented).Dump();
}

public class AppData
{
  [JsonDefaultValueAttribute(typeof(IEnumerable<RangeData>), @"[
    {
      ""Name"": ""First"",
      ""Minimum"": 120,
      ""Maximum"": 145,
      ""Include"": true
    },
    {
      ""Name"": ""Second"",
      ""Minimum"": 125,
      ""Maximum"": 135,
      ""Include"": false
    }
  ]")]
  public IEnumerable<RangeData> Ranges
  {
    get;
    set;
  }

  [JsonDefaultValueAttribute(typeof(IEnumerable<Highlight>), "[ ]")]
  [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
  public IEnumerable<Highlight> Highlights
  {
    get;
    set;
  }

  [JsonDefaultValueAttribute(typeof(IEnumerable<string>), "[ ]")]
  [JsonProperty(DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
  public IEnumerable<string> Filters
  {
    get;
    set;
  }
}

public class RangeData
{
  public string Name { get; set; }
  public int Minimum { get; set; }
  public int Maximum { get; set; }
  public bool Include { get; set; }
}

public class Highlight
{
  public string Text { get; set; }
  public Brush Color { get; set; }
}

public class JsonDefaultValueAttribute : DefaultValueAttribute
{
  public JsonDefaultValueAttribute(Type type, string json)
    : base(ConvertFromJson(type, json))
  {
  }

  private static object ConvertFromJson(Type type, string json)
  {
    var value = JsonConvert.DeserializeObject(json, type, new JsonSerializerSettings
    {
      MissingMemberHandling = MissingMemberHandling.Error,
      NullValueHandling = NullValueHandling.Include,
      DefaultValueHandling = DefaultValueHandling.Populate
    });

    return value;
  }
}

Output from this is

AppData >
UserQuery+AppData 

Ranges       null 
Highlights   null 
Filters      null 

AppData >
UserQuery+AppData 

Ranges       null
Highlights   (0 items)
Filters      (0 items) 

AppData >
UserQuery+AppData 

Ranges       List<RangeData> (2 items) >
             Name     Minimum    Maximum    Include
             Foo      110        120        True 
             Bar      125        145        True 
Highlights   (0 items) 
Filters      (0 items) 

{
  "Ranges": [
    {
      "Name": "Foo",
      "Minimum": 110,
      "Maximum": 120,
      "Include": true
    },
    {
      "Name": "Bar",
      "Minimum": 125,
      "Maximum": 145,
      "Include": true
    }
  ]
}

This appears to deserialize correctly, but only Highlights and Filters give me collections as expected. Stepping through the debug reveals the data is correctly parsed and put into the correct structure. Any insights to understand what I'm missing with respect to the collection with data?

EDIT: Added parameter to overwrite existing objects if there is data on call to PopulateObject

Ron O
  • 89
  • 7
  • Note the comment from the question to which you linked: *however, there's a problem: DefaultValueAttribute.Value is just an object, and your Bar class is mutable, so setting it in Foo will save a reference to the **global instance** everywhere - which will then get modified.* – dbc Sep 03 '17 at 22:54
  • Right. But that's not what I'm doing. I'm not preserving as in the example. I'm only setting it as the default and allowing JSON.net to use it as the default value (at least that's the intention). Even so, why does it work for Highlights and Filters? – Ron O Sep 05 '17 at 00:55
  • Interestingly, I added a workaround to pre-populate the Ranges variable in the AppData ctor. I discovered the default behavior adds to existing collections. I've updated the example above to include the ObjectCreationHandling set to replace existing values. I added the method below in AppData to dump the default. It remains as defined in the attribute. public T GetDefaultValue(string name) { var prop = GetType().GetProperty(name); var d = prop.GetCustomAttribute(); if (d != null) return (T)d.Value; return default(T); } – Ron O Sep 05 '17 at 16:59
  • That's correct. See [Why are all the collections in my POCO are null when deserializing some valid json with the .NET Newtonsoft.Json component](https://stackoverflow.com/q/32491966) for an explanation of why the pre-populated collections are added to, and how to work around the issue. See [Populate object where objects are reused and arrays are replaced?](https://stackoverflow.com/q/42165648) for another workaround. – dbc Sep 05 '17 at 17:01
  • Committed the comment too early. Full edit explains what I did. It still does not answer the original question. Your subsequent comment does not address my situation. I have no issues with deserialization (even before adding JsonSerializerSettings to my PopulateObject call). The JsonDefaultValue handling already included JsonSerializerSettings. – Ron O Sep 05 '17 at 17:05

0 Answers0