When dealing with large JSON objects, it is important not to load the entire JSON stream into an intermediate representation before final deserialization. Thus:
Do not download the JSON as a string. From Performance Tips:
To minimize memory usage and the number of objects allocated, Json.NET supports serializing and deserializing directly to a stream. Reading or writing JSON a piece at a time, instead of having the entire JSON string loaded into memory, is especially important when working with JSON documents greater than 85kb in size to avoid the JSON string ending up in the large object heap.
Instead, Newtonsoft recommends to deserialize directly from the response stream, e.g.:
HttpClient client = new HttpClient();
using (Stream s = client.GetStreamAsync("http://www.test.com/large.json").Result)
using (StreamReader sr = new StreamReader(s))
using (JsonReader reader = new JsonTextReader(sr))
{
JsonSerializer serializer = new JsonSerializer();
// read the json from a stream
// json size doesn't matter because only a small piece is read at a time from the HTTP request
RootObject root = serializer.Deserialize<RootObject>(reader);
}
Do not load your entire JSON into a JArray
simply in order to deserialize the "result"
value. Instead stream through the JSON with a JsonTextReader
until you find a property named "result"
and then deserialize its value, as is shown in JSON.NET deserialize a specific property.
To automatically map all non-collection-valued object properties from and to single-entry arrays, you can create a custom IContractResolver
that applies an appropriate custom JsonConverter
to properties of the appropriate type.
Putting all this together, you need the following extension methods and contract resolver:
public static class JsonExtensions
{
public static IEnumerable<T> DeserializeNamedProperties<T>(Stream stream, string propertyName, JsonSerializerSettings settings = null, int? depth = null)
{
using (var textReader = new StreamReader(stream))
foreach (var value in DeserializeNamedProperties<T>(textReader, propertyName, settings, depth))
yield return value;
}
public static IEnumerable<T> DeserializeNamedProperties<T>(TextReader textReader, string propertyName, JsonSerializerSettings settings = null, int? depth = null)
{
var serializer = JsonSerializer.CreateDefault(settings);
using (var jsonReader = new JsonTextReader(textReader))
{
while (jsonReader.Read())
{
if (jsonReader.TokenType == JsonToken.PropertyName
&& (string)jsonReader.Value == propertyName
&& depth == null || depth == jsonReader.Depth)
{
jsonReader.Read();
yield return serializer.Deserialize<T>(jsonReader);
}
}
}
}
}
public class ArrayToSingleContractResolver : DefaultContractResolver
{
// As of 7.0.1, Json.NET suggests using a static instance for "stateless" contract resolvers, for performance reasons.
// http://www.newtonsoft.com/json/help/html/ContractResolver.htm
// http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_Serialization_DefaultContractResolver__ctor_1.htm
// "Use the parameterless constructor and cache instances of the contract resolver within your application for optimal performance."
static ArrayToSingleContractResolver instance;
static ArrayToSingleContractResolver() { instance = new ArrayToSingleContractResolver(); }
public static ArrayToSingleContractResolver Instance { get { return instance; } }
readonly SimplePropertyArrayToSingleConverter simpleConverter = new SimplePropertyArrayToSingleConverter();
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var jsonProperty = base.CreateProperty(member, memberSerialization);
if (jsonProperty.Converter == null && jsonProperty.MemberConverter == null)
{
if (jsonProperty.PropertyType.IsPrimitive
|| jsonProperty.PropertyType == typeof(string))
{
jsonProperty.Converter = jsonProperty.MemberConverter = simpleConverter;
}
else if (jsonProperty.PropertyType != typeof(object)
&& !typeof(IEnumerable).IsAssignableFrom(jsonProperty.PropertyType)
&& !typeof(JToken).IsAssignableFrom(jsonProperty.PropertyType))
{
jsonProperty.Converter = jsonProperty.MemberConverter = new ObjectPropertyArrayToSingleConverter(this, jsonProperty.PropertyType);
}
}
return jsonProperty;
}
}
public static class JsonContractExtensions
{
public static bool? IsArrayContract(this JsonContract contract)
{
if (contract == null)
throw new ArgumentNullException();
if (contract is JsonArrayContract)
return true;
else if (contract is JsonLinqContract)
return null; // Could be an object or an array.
else
return false;
}
}
class SimplePropertyArrayToSingleConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
throw new NotImplementedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
while (reader.TokenType == JsonToken.Comment)
reader.Read();
if (reader.TokenType == JsonToken.Null)
return null;
var contract = serializer.ContractResolver.ResolveContract(objectType);
bool hasValue = false;
if (reader.TokenType == JsonToken.StartArray)
{
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonToken.Comment:
break;
case JsonToken.EndArray:
return UndefaultValue(objectType, existingValue, contract);
default:
if (hasValue)
throw new JsonSerializationException("Too many values at path: " + reader.Path);
existingValue = ReadItem(reader, objectType, existingValue, serializer, contract);
hasValue = true;
break;
}
}
// Should not come here.
throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
}
else
{
existingValue = ReadItem(reader, objectType, existingValue, serializer, contract);
return UndefaultValue(objectType, existingValue, contract);
}
}
private static object UndefaultValue(Type objectType, object existingValue, JsonContract contract)
{
if (existingValue == null && objectType.IsValueType && Nullable.GetUnderlyingType(objectType) == null)
existingValue = contract.DefaultCreator();
return existingValue;
}
private static object ReadItem(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer, JsonContract contract)
{
if (contract is JsonPrimitiveContract || existingValue == null)
{
existingValue = serializer.Deserialize(reader, objectType);
}
else
{
serializer.Populate(reader, existingValue);
}
return existingValue;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteStartArray();
if (value != null)
serializer.Serialize(writer, value);
writer.WriteEndArray();
}
}
class ObjectPropertyArrayToSingleConverter : SimplePropertyArrayToSingleConverter
{
readonly Type propertyType;
readonly IContractResolver resolver;
int canConvert = -1;
public ObjectPropertyArrayToSingleConverter(IContractResolver resolver, Type propertyType)
: base()
{
if (propertyType == null || resolver == null)
throw new ArgumentNullException();
this.propertyType = propertyType;
this.resolver = resolver;
}
int GetIsEnabled()
{
var contract = resolver.ResolveContract(propertyType);
return contract.IsArrayContract() == false ? 1 : 0;
}
bool IsEnabled
{
get
{
// We need to do this in a lazy fashion since recursive calls to resolve contracts while creating a contract are problematic.
if (canConvert == -1)
Interlocked.Exchange(ref canConvert, GetIsEnabled());
return canConvert == 1;
}
}
public override bool CanRead { get { return IsEnabled; } }
public override bool CanWrite { get { return IsEnabled; } }
}
Then use it like:
string url = @"..."; // Replace with your actual URL.
IList<BusinessFunctionData> outputlist;
WebRequest request = WebRequest.Create(url);
using (var response = request.GetResponse())
using (var responseStream = response.GetResponseStream())
{
var settings = new JsonSerializerSettings { ContractResolver = ArrayToSingleContractResolver.Instance, NullValueHandling = NullValueHandling.Ignore };
outputlist = JsonExtensions.DeserializeNamedProperties<List<BusinessFunctionData>>(responseStream, "result", settings).FirstOrDefault();
}