I don't believe I am wrapping my head around how to properly use JsonConverter for polymorphism in parsing json results.
In my scenario, I am targeting Git Policy Configurations in TFS. A policy configuration:
"value": [
{
"createdBy": {
"displayName": "username",
"url": "url",
"id": "id",
"uniqueName": "user",
"imageUrl": "url"
},
"createdDate": "2020-03-21T18:17:24.3240783Z",
"isEnabled": true,
"isBlocking": true,
"isDeleted": false,
"settings": {
"minimumApproverCount": 1,
"creatorVoteCounts": false,
"allowDownvotes": false,
"resetOnSourcePush": true,
"scope": [{
"refName": "refs/heads/master",
"matchKind": "Exact",
"repositoryId": "id"
}
]
},
"_links": {
"self": {
"href": "url"
},
"policyType": {
"href": "url"
}
},
"revision": 1,
"id": 974,
"url": "url",
"type": {
"id": "id",
"url": "url",
"displayName": "Minimum number of reviewers"
},
{...}]
More settings examples:
Require a Merge Strategy
"settings": {
"useSquashMerge": true,
"scope": [
{
"refName": "refs/heads/master",
"matchKind": "Exact",
"repositoryId": "id"
}
]
}
Required Reviewers
"settings": {
"requiredReviewerIds": [
"id"
],
"scope": [
{
"refName": "refs/heads/master",
"matchKind": "Exact",
"repositoryId": "id"
}
]
}
In the json snippet above, the settings object is different based on the type of configuration.
What is the best approach to writing a converter than can dynamically serialize/deserialize the settings object? I've read a couple of articles regarding this and can't quite wrap my head around it.
This is how I am currently deserializing all of my API results, so far they have been simple result sets.
async Task<List<T>> ParseResults<T>( HttpResponseMessage result, string parameter )
{
List<T> results = new List<T>();
if ( result.IsSuccessStatusCode )
{
using var stream = await result.Content.ReadAsStreamAsync();
JsonDocument doc = JsonDocument.Parse( stream );
JsonElement collection = doc.RootElement.GetProperty( parameter ).Clone();
foreach ( var item in collection.EnumerateArray() )
{
results.Add( JsonSerializer.Deserialize<T>( item.ToString() ) );
}
}
return results;
}
My integration test.
PolicyConfiguration is the type I am trying to deserialize to.
[Test]
public async Task Get_TestMasterBranchPolicyConfigurations()
{
HttpResponseMessage result = await GetResult( $"{_collection}/ProductionBuildTesting/_apis/policy/configurations?api-version=4.1" );
List<PolicyConfiguration> configurations = await ParseResults<PolicyConfiguration>( result, "value" );
Assert.AreEqual( 16, configurations.Count );
JsonPrint( configurations );
}
My current classes for this parsing situation
public class CreatedBy
{
[JsonPropertyName( "displayName" )]
public string DisplayName { get; set; }
[JsonPropertyName( "url" )]
public string Url { get; set; }
[JsonPropertyName( "id" )]
public Guid Id { get; set; }
[JsonPropertyName( "uniqueName" )]
public string UniqueName { get; set; }
[JsonPropertyName( "imageUrl" )]
public string ImageUrl { get; set; }
}
public class PolicyConfigurationScope
{
[JsonPropertyName( "refName" )]
public string RefName { get; set; }
[JsonPropertyName( "matchKind" )]
public string MatchKind { get; set; }
[JsonPropertyName( "repositoryId" )]
public Guid RepositoryId { get; set; }
}
public class PolicyConfigurationSettings_MinimumNumberOfReviewers
{
[JsonPropertyName( "minimumApproverCount" )]
public int MinimumApproverCount { get; set; }
[JsonPropertyName( "creatorVoteCounts" )]
public bool CreatorVoteCounts { get; set; }
[JsonPropertyName( "allowDownvotes" )]
public bool AllowDownvotes { get; set; }
[JsonPropertyName( "resetOnSourcePush" )]
public bool ResetOnSourcePush { get; set; }
[JsonPropertyName( "scope" )]
public List<PolicyConfigurationScope> Scope { get; set; }
}
public class PolicyConfigurationType
{
[JsonPropertyName( "id" )]
public Guid Id { get; set; }
[JsonPropertyName( "url" )]
public string Url { get; set; }
[JsonPropertyName( "displayName" )]
public string DisplayName { get; set; }
}
public class PolicyConfiguration
{
[JsonPropertyName( "createdBy" )]
public CreatedBy CreatedBy { get; set; }
[JsonPropertyName( "createdDate" )]
public DateTime CreatedDate { get; set; }
[JsonPropertyName( "isEnabled" )]
public bool IsEnabled { get; set; }
[JsonPropertyName( "isBlocking" )]
public bool IsBlocking { get; set; }
[JsonPropertyName( "isDeleted" )]
public bool IsDeleted { get; set; }
//[JsonPropertyName( "settings" )]
//public PolicyConfigurationSettings_MinimumNumberOfReviewersSettings Settings { get; set; }
[JsonPropertyName( "revision" )]
public int Revision { get; set; }
[JsonPropertyName( "id" )]
public int Id { get; set; }
[JsonPropertyName( "url" )]
public string Url { get; set; }
[JsonPropertyName( "type" )]
public PolicyConfigurationType Type { get; set; }
}
I ended up solving my issue in slightly the same way I had seen a previous article using a discriminator. Since I do not control the API feeds, I do not have a discriminator to drive off of, so I am relying on the properties of the Json object.
Need to create a Converter:
public class PolicyConfigurationSettingsConverter : JsonConverter<PolicyConfigurationSettings>
{
public override PolicyConfigurationSettings Read( ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options )
{
JsonDocument doc;
JsonDocument.TryParseValue( ref reader, out doc );
if ( doc.RootElement.TryGetProperty( "minimumApproverCount", out _ ) )
return JsonSerializer.Deserialize<MinimumNumberOfReviewers>( doc.RootElement.ToString(), options );
if ( doc.RootElement.TryGetProperty( "useSquashMerge", out _ ) )
return JsonSerializer.Deserialize<RequireAMergeStrategy>( doc.RootElement.ToString(), options );
if ( doc.RootElement.TryGetProperty( "scope", out _ ) )
return JsonSerializer.Deserialize<PolicyConfigurationSettingsScope>( doc.RootElement.ToString(), options );
return null;
}
public override void Write( Utf8JsonWriter writer, [DisallowNull] PolicyConfigurationSettings value, JsonSerializerOptions options )
{
if ( value.GetType() == typeof( MinimumNumberOfReviewers ) )
JsonSerializer.Serialize( writer, ( MinimumNumberOfReviewers )value, options );
if ( value.GetType() == typeof( RequireAMergeStrategy ) )
JsonSerializer.Serialize( writer, ( RequireAMergeStrategy )value, options );
if ( value.GetType() == typeof( PolicyConfigurationSettingsScope ) )
JsonSerializer.Serialize( writer, ( PolicyConfigurationSettingsScope )value, options );
}
}
Then need to create a JsonSerializerOptions object to add the Converter
public static JsonSerializerOptions PolicyConfigurationSettingsSerializerOptions()
{
var serializeOptions = new JsonSerializerOptions();
serializeOptions.Converters.Add( new PolicyConfigurationSettingsConverter() );
return serializeOptions;
}
Pass the options into your Serializer/Deserializer statement.
Below is the PolicyConfigurationSettings class
public abstract class PolicyConfigurationSettings
{
[JsonPropertyName( "scope" )]
public List<PolicyConfigurationScope> Scope { get; set; }
}
public class MinimumNumberOfReviewers : PolicyConfigurationSettings
{
[JsonPropertyName( "minimumApproverCount" )]
public int MinimumApproverCount { get; set; }
[JsonPropertyName( "creatorVoteCounts" )]
public bool CreatorVoteCounts { get; set; }
[JsonPropertyName( "allowDownvotes" )]
public bool AllowDownvotes { get; set; }
[JsonPropertyName( "resetOnSourcePush" )]
public bool ResetOnSourcePush { get; set; }
}
public class RequireAMergeStrategy : PolicyConfigurationSettings
{
[JsonPropertyName( "useSquashMerge" )]
public bool UseSquashMerge { get; set; }
}
public class PolicyConfigurationSettingsScope : PolicyConfigurationSettings { }
In net 5.0 with System.Text.Json.JsonSerializer, what works for a class like this:
public class A
{
public B Data { get; set; }
}
public class B
{
public long Count { get; set; }
}
is using:
System.Text.Json.JsonSerializer.Deserialize<A>("{{\"data\":{\"count\":10}}}", new JsonSerializerOptions { PropertyNameCaseInsensitive = true, IncludeFields = true })
which is weird that is not the default.
I solved this with a more generic approach, that falls somewhere between the way NewtonSoft Json and the .NET Json work.
Using a custom converter, I serialize any polymorphic class, using a type identifier similar to the Newtonsoft approach, but to mitigate the possible security risk you can chose to allow only internal types or types from a specific assembly.
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics;
using System.Collections.ObjectModel;
public class JsonConverterEx<T> : System.Text.Json.Serialization.JsonConverter<T>
{
private bool _internalOnly = true;
private string _assembly = String.Empty;
public JsonConverterEx()
{
this._assembly = this.GetType().Assembly.FullName;
}
public JsonConverterEx(bool bInternalOnly, string assemblyName)
{
_internalOnly = bInternalOnly;
_assembly = assemblyName;
}
public override bool CanConvert(Type typeToConvert)
{
Type t = typeof(T);
if(typeToConvert == t)
return true;
return false;
}
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
validateToken(reader, JsonTokenType.StartObject);
reader.Read(); // Move to property name
validateToken(reader, JsonTokenType.PropertyName);
var typeKey = reader.GetString();
reader.Read(); // Move to start of object (stored in this property)
validateToken(reader, JsonTokenType.StartObject);
if(!_internalOnly)
{
typeKey += ", " + _assembly;
}
Type t = Type.GetType(typeKey);
if(t != null)
{
T o = (T)JsonSerializer.Deserialize(ref reader, t, options);
reader.Read(); // Move past end of item object
return o;
}
else
{
throw new JsonException($"Unknown type '{typeKey}'");
}
// Helper function for validating where you are in the JSON
void validateToken(Utf8JsonReader reader, JsonTokenType tokenType)
{
if(reader.TokenType != tokenType)
throw new JsonException($"Invalid token: Was expecting a '{tokenType}' token but received a '{reader.TokenType}' token");
}
}
public override void Write(Utf8JsonWriter writer, [DisallowNull] T value, JsonSerializerOptions options)
{
var itemType = value.GetType();
writer.WriteStartObject();
writer.WritePropertyName(itemType.FullName);
// pass on to default serializer
JsonSerializer.Serialize(writer, value, itemType, options);
writer.WriteEndObject();
}
}
How to use it:
JsonSerializerOptions op = new JsonSerializerOptions()
{
// your usual options here
};
op.Converters.Add(new JsonConverterEx<MyExternalClass>(false, "MyAssembly"));
op.Converters.Add(new JsonConverterEx<MyInternalClass>());
string s = System.Text.Json.JsonSerializer.Serialize(myobj, op);
MyInternalClass c = System.Text.Json.JsonSerializer.Deserialize<MyInternalClass>(s, op);
Alternatively, a more flexible design for serialization
public class PolymorphicConverter<T> : JsonConverter<T> where T : class
{
public override T Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options)
{
throw new NotImplementedException();
}
public override void Write(
Utf8JsonWriter writer,
[DisallowNull] T value,
JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}
next, you can turn on your fantasy and customize deserialization
Do not forget:
options.JsonSerializerOptions.Converters.Add(new PolymorphicConverter<IFucker>());
Related
When using the request/response pattern, during the deserialization of the response, an incompatibility error occurs. Apparently, the publisher serializer configuration is causing this issue, although the message is as expected.
Using the JsonProperty feature could isolate the problem, however, it does not reflect what was expected.
Versions
.NET: 6.0;
MassTRansit: 7.3.0;
Newtonsoft: 13.0.1
Type specified in JSON 'Messages.Models+CreditCard, Messages'
is not compatible with 'GreenPipes.DynamicInternal.Models\+Messages.Models\+IPaymentMethod'
Resource:
_bus.CreateRequestClient<TMessage>().GetResponse<TResponse>(message, cancellationToken);
Publisher serializer configuration:
bus.ConfigureJsonSerializer(settings =>
{
settings.TypeNameHandling = TypeNameHandling.Objects;
return settings;
});
Message response serialization:
{
"message": {
"$type": "Messages.Services.ShoppingCarts.Responses+CartDetails, Messages",
"cartItems": [
{
"$type": "Messages.Models+Item, Messages",
"productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"productName": "string",
"unitPrice": "10.0",
"quantity": 30,
"pictureUrl": "string"
}
],
"paymentMethods": [
{
"$type": "Messages.Models+CreditCard, Messages",
"id": "be7c40ac-1cd1-4e35-bccf-a1a2f4efecfd",
"expiration": "01/22",
"holderName": "string",
"number": "374245455400126",
"securityNumber": "string"
},
{
"$type": "Messages.Models+PayPal, Messages",
"id": "9465cf12-a322-477d-94c8-116d03a8399e",
"Password": "123",
"UserName": "string"
}
],
...
}
}
Consumer deserializer configuration:
bus.ConfigureJsonDeserializer(settings =>
{
settings.TypeNameHandling = TypeNameHandling.Objects; // or Auto, All, etc...
return settings;
});
Error:
System.Runtime.Serialization.SerializationException: A JSON serialization exception occurred while deserializing the message envelope
---> Newtonsoft.Json.JsonSerializationException: Type specified in JSON 'Messages.Models+CreditCard, Messages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'
is not compatible with 'GreenPipes.DynamicInternal.Models\+Messages.Models\+IPaymentMethod, MessagesGreenPipes.DynamicInternale7ccc67139ad479db488c4fa6310335a, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.
Path 'message.paymentMethods[0].$type', line 30, position 55.
JsonProperty does not work:
public record CartDetails : Response
{
[JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
public IEnumerable<Models.IPaymentMethod> PaymentMethods { get; init; }
}
{
"message": {
"cartItems": [
{
"productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"productName": "string",
"unitPrice": "10.0",
"quantity": 30,
"pictureUrl": "string"
}
],
"paymentMethods": [
{
"id": "be7c40ac-1cd1-4e35-bccf-a1a2f4efecfd",
"expiration": "01/22",
"holderName": "string",
"number": "374245455400126",
"securityNumber": "string"
},
{
"id": "9465cf12-a322-477d-94c8-116d03a8399e",
"Password": "123",
"UserName": "string"
}
],
...
}
}
Response
public record CartDetails : Response
{
// [JsonProperty(TypeNameHandling = TypeNameHandling.Objects)]
public IEnumerable<Models.IPaymentMethod> PaymentMethods { get; init; }
public IEnumerable<Models.Item> CartItems { get; init; }
public Guid UserId { get; init; }
public decimal Total { get; init; }
public Guid Id { get; init; }
public bool IsDeleted { get; init; }
}
Types:
public interface IPaymentMethod
{
Guid Id { get; }
decimal Amount { get; }
}
public record CreditCard : IPaymentMethod
{
public Guid Id { get; init; }
public decimal Amount { get; init; }
[property: JsonConverter(typeof(ExpirationDateOnlyJsonConverter))]
public DateOnly Expiration { get; init; }
public string HolderName { get; init; }
public string Number { get; init; }
public string SecurityNumber { get; init; }
}
public record PayPal : IPaymentMethod
{
public Guid Id { get; init; }
public decimal Amount { get; init; }
public string UserName { get; init; }
public string Password { get; init; }
}
Thanks Wiktor Zychla, the issue led me to Nkot's workaround, which served me very well:
TypeNameHandlingConverter:
internal class TypeNameHandlingConverter : JsonConverter
{
private readonly TypeNameHandling _typeNameHandling;
public TypeNameHandlingConverter(TypeNameHandling typeNameHandling)
{
_typeNameHandling = typeNameHandling;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
=> new JsonSerializer { TypeNameHandling = _typeNameHandling }.Serialize(writer, value);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
=> new JsonSerializer { TypeNameHandling = _typeNameHandling }.Deserialize(reader, objectType);
public override bool CanConvert(Type objectType)
=> IsMassTransitOrSystemType(objectType) is false;
private static bool IsMassTransitOrSystemType(Type objectType)
=> objectType.Assembly == typeof(IConsumer).Assembly ||
objectType.Assembly.IsDynamic ||
objectType.Assembly == typeof(object).Assembly;
}
Consumer deserializer configuration:
bus.ConfigureJsonDeserializer(settings =>
{
settings.Converters.Add(new TypeNameHandlingConverter(TypeNameHandling.Objects));
return settings;
});
Context:
Cust has a service that send the following json. He can easly change the target of that query but not the query it self.
I have to build a WebService that accept query like the following JSON.
While I will have no issue handeling the Json, I have an issue trying to define the method/interface that will accept a query like this.
The issue comes from Houses > Things: It's a dictionary of string, "objectThing" where "objectThing" has a property value that may hold multiple type.
EG:
int , "Value": 42
string , "Value": "Catty"
string array, "Value": ["Book1", "Book2", "Book3"]
object , A limited List of Object Type
"Value":
{
"PeopleId": "1234ABCD",
"Name": "John"
}
object array, An Array of with limited List of Object Type
"Value": [
{
"PeopleId": "1234ABCD",
"Name": "John"
},
{
"PeopleId": "0000AAAA",
"Name": "Doe"
}
]
Value is not Dynamic for me. It's within a limited list of Type that I can define.
Json Example:
{
"RootID" : "0123456",
"FooID" : "0123456",
"BarID" : "0123456",
"Houses" :[
{
"OwnerId" : "0123456",
"Date" : 1890895600000,
"Location" : {
"Latitude" : -1,
"Longitude" : -1
},
"Things" :{
"1" :{
"Label": "Books",
"Type" : "List",
"Value": ["Book1", "Book2", "Book3"]
},
"2" :{
"Label": "Cat",
"Type" : "Text",
"Value": "Catty"
},
"3" :{
"Label": "A Number",
"Type" : "Int",
"Value": 42
},
"4" :{
"Label": "Peoples",
"Type" : "People",
"Value": [
{
"PeopleId": "1234ABCD",
"Name": "John"
},
{
"PeopleId": "0000AAAA",
"Name": "Doe"
}
]
}
}
},
{
"OwnerId" : "111111",
"Things" :{}
},
{
"OwnerId" : "000001",
"Things" :{}
}
]
}
And the Class definition, If I were to deserialize this Json into a proper type:
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
public partial class QueryRoot
{
[JsonProperty("RootID")]
public string RootId { get; set; }
[JsonProperty("FooID")]
public string FooId { get; set; }
[JsonProperty("BarID")]
public string BarId { get; set; }
[JsonProperty("Houses")]
public List<House> Houses { get; set; }
}
public partial class House
{
[JsonProperty("OwnerId")]
public string OwnerId { get; set; }
[JsonProperty("Date", NullValueHandling = NullValueHandling.Ignore)]
public long? Date { get; set; }
[JsonProperty("Location", NullValueHandling = NullValueHandling.Ignore)]
public Location Location { get; set; }
[JsonProperty("Things")]
public Dictionary<string, Thing> Things { get; set; }
}
public partial class Location
{
[JsonProperty("Latitude")]
public long Latitude { get; set; }
[JsonProperty("Longitude")]
public long Longitude { get; set; }
}
public partial class Thing
{
[JsonProperty("Label")]
public string Label { get; set; }
[JsonProperty("Type")]
public string Type { get; set; }
[JsonProperty("Value")]
public ThingValue Value { get; set; }
}
public partial class ValueClass
{
[JsonProperty("PeopleId")]
public string PeopleId { get; set; }
[JsonProperty("Name")]
public string Name { get; set; }
}
public partial struct ValueElement
{
public string String;
public ValueClass ValueClass;
public static implicit operator ValueElement(string String) => new ValueElement { String = String };
public static implicit operator ValueElement(ValueClass ValueClass) => new ValueElement { ValueClass = ValueClass };
}
public partial struct ThingValue
{
public List<ValueElement> AnythingArray;
public long? Integer;
public string String;
public static implicit operator ThingValue(List<ValueElement> AnythingArray) => new ThingValue { AnythingArray = AnythingArray };
public static implicit operator ThingValue(long Integer) => new ThingValue { Integer = Integer };
public static implicit operator ThingValue(string String) => new ThingValue { String = String };
}
public partial class QueryRoot
{
public static QueryRoot FromJson(string json) => JsonConvert.DeserializeObject<QueryRoot>(json, QuickType.Converter.Settings);
}
public static class Serialize
{
public static string ToJson(this QueryRoot self) => JsonConvert.SerializeObject(self, QuickType.Converter.Settings);
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
ThingValueConverter.Singleton,
ValueElementConverter.Singleton,
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
internal class ThingValueConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(ThingValue) || t == typeof(ThingValue?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.Integer:
var integerValue = serializer.Deserialize<long>(reader);
return new ThingValue { Integer = integerValue };
case JsonToken.String:
case JsonToken.Date:
var stringValue = serializer.Deserialize<string>(reader);
return new ThingValue { String = stringValue };
case JsonToken.StartArray:
var arrayValue = serializer.Deserialize<List<ValueElement>>(reader);
return new ThingValue { AnythingArray = arrayValue };
}
throw new Exception("Cannot unmarshal type ThingValue");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
var value = (ThingValue)untypedValue;
if (value.Integer != null)
{
serializer.Serialize(writer, value.Integer.Value);
return;
}
if (value.String != null)
{
serializer.Serialize(writer, value.String);
return;
}
if (value.AnythingArray != null)
{
serializer.Serialize(writer, value.AnythingArray);
return;
}
throw new Exception("Cannot marshal type ThingValue");
}
public static readonly ThingValueConverter Singleton = new ThingValueConverter();
}
internal class ValueElementConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(ValueElement) || t == typeof(ValueElement?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.String:
case JsonToken.Date:
var stringValue = serializer.Deserialize<string>(reader);
return new ValueElement { String = stringValue };
case JsonToken.StartObject:
var objectValue = serializer.Deserialize<ValueClass>(reader);
return new ValueElement { ValueClass = objectValue };
}
throw new Exception("Cannot unmarshal type ValueElement");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
var value = (ValueElement)untypedValue;
if (value.String != null)
{
serializer.Serialize(writer, value.String);
return;
}
if (value.ValueClass != null)
{
serializer.Serialize(writer, value.ValueClass);
return;
}
throw new Exception("Cannot marshal type ValueElement");
}
public static readonly ValueElementConverter Singleton = new ValueElementConverter();
}
I already have a WCF Service that handles Json. It work fine the issue is declaring the method/interface that will accept this kind of query.
If WCF Web Service is a limiting factor, or if (ASP.NET/Core) Web APIs provides an easier path it's welcom.
You can receive a JSON string and convert it to an object. Here is a demo:
[WebMethod]
public string HelloWorld()
{
Stream s = HttpContext.Current.Request.InputStream;
byte[] b = new byte[s.Length];
s.Read(b, 0, (int)s.Length);
string jsontext = Encoding.UTF8.GetString(b);
var productProperty = JsonHelper.JsonDeserialize<School>(jsontext); //Deserialize JSON strings to objects
return "Hello World";
}
This is the method in WebService.
[DataContract]
public class School
{
[DataMember]
public int ClassroomId { set; get; }
[DataMember]
public List<Student> StudentList { set; get; }
}
[DataContract]
public class Student
{
[DataMember]
public int StudentId { set; get; }
[DataMember]
public string StudentName { set; get; }
}
This is the object to be converted by JSON string.
public class JsonHelper
{
public static string JsonSerializer<T>(T t)
{
var ser = new DataContractJsonSerializer(typeof(T));
var ms = new MemoryStream();
ser.WriteObject(ms, t);
string jsonString = Encoding.UTF8.GetString(ms.ToArray());
ms.Close();
return jsonString;
}
public static T JsonDeserialize<T>(string jsonString)
{
var ser = new DataContractJsonSerializer(typeof(T));
var ms = new MemoryStream(Encoding.UTF8.GetBytes(jsonString));
var obj = (T)ser.ReadObject(ms);
return obj;
}
}
To deserialize JSON characters into objects,there are many open-source class libraries. I use the DatacontractJsonSerializer that comes with .net version 3.5 or above.I wrote a JsonHelper class.
I have to get back an object the frontend is sending me. Thing is, front team wanted their component to be as generic as it can be so, in the dto, i'll recieve a Value property that can be different things (a boolean, a string, a list a strings, a numeric value...), as following :
"conditions": [
{
"alias": "FSTNM",
"providerKey": "Marketing",
"hasValue": true,
"conditionType": "Text",
"values": "john",
"startDate": null,
"endDate": null
},
{
"alias": "LSTNM",
"providerKey": "Marketing",
"hasValue": true,
"conditionType": "Text",
"values": null,
"startDate": null,
"endDate": null
},
{
"alias": "BTHDT",
"providerKey": "Marketing",
"hasValue": true,
"conditionType": "DateTime",
"values": null,
"startDate": "02-10-1980",
"endDate": "17-08-1989"
},
{
"alias": "AMECH",
"providerKey": "Custom",
"hasValue": true,
"conditionType": "Boolean",
"values": true,
"startDate": null,
"endDate": null
},
{
"alias": "CMBCH",
"providerKey": "Custom",
"hasValue": true,
"conditionType": "Number",
"values": 2,
"startDate": null,
"endDate": null
},
{
"alias": "FVRDR",
"providerKey": "Custom",
"hasValue": true,
"conditionType": "List",
"values": [
1,
3
],
"startDate": null,
"endDate": null
}
]
So, I tried to set the Values property as object like this :
public class DataTableFilterValueDTO
{
public string Alias { get; set; }
public string ProviderKey { get; set; }
public bool HasValue { get; set; }
public string ConditionType { get; set; }
public object Values { get; set; }
public string StartDate { get; set; }
public string EndDate { get; set; }
}
It seems that it works. When the request reack my controller, Values seems to be the right type, and depending on conditionType, I can cast it to the object i'm supposed to retrieve, like this :
If conditionType = "Text" :
var values = condition.Values as string;
If conditionType = "Boolean" :
var values = condition.Values as bool;
If conditionType = "List" :
var values = condition.Values as List<string>;
But is this a thing to do?
I mean, it does not feel right using this but I've never worked with the objecttype and can't really tell when it's good to use it.
You could implement a Custom JsonConverter. To do this, declare your class as abstract, and define a class that inherits it for each data type you expect to receive, like so:
[JsonConverter(typeof(ValueConverter))]
public abstract class Value
{
public string Alias { get; set; }
public string ProviderKey { get; set; }
public bool HasValue { get; set; }
public string ConditionType { get; set; }
public string StartDate { get; set; }
public string EndDate { get; set; }
}
public class BooleanValue : Value { public bool? Values { get; set; } }
public class ListValue : Value { public List<string> Values { get; set; } }
public class StringValue : Value { public string Values { get; set; } }
public class DateTimeValue : Value { public DateTime? Values { get; set; } }
public class IntegerValue : Value { public int? Values { get; set; } }
Then you need to define your custom JsonConverter where you perform the type conversion according to the value of the conditionType property:
public class ValueConverter : JsonConverter
{
static readonly JsonSerializerSettings SpecifiedSubclassConversion = new JsonSerializerSettings() { ContractResolver = new CustomResolver() };
public override bool CanConvert(Type objectType) => objectType == typeof(Value);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject obj = JObject.Load(reader);
return (obj["conditionType"].Value<string>()) switch
{
"Text" => JsonConvert.DeserializeObject<StringValue>(obj.ToString(), SpecifiedSubclassConversion),
"DateTime" => JsonConvert.DeserializeObject<DateTimeValue>(obj.ToString(), SpecifiedSubclassConversion),
"Boolean" => JsonConvert.DeserializeObject<BooleanValue>(obj.ToString(), SpecifiedSubclassConversion),
"Number" => JsonConvert.DeserializeObject<IntegerValue>(obj.ToString(), SpecifiedSubclassConversion),
"List" => JsonConvert.DeserializeObject<ListValue>(obj.ToString(), SpecifiedSubclassConversion),
_ => throw new Exception("Unknown conditionType"),
};
throw new NotImplementedException();
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
public class CustomResolver: DefaultContractResolver
{
protected override JsonConverter ResolveContractConverter(Type objectType)
{
if (typeof(Value).IsAssignableFrom(objectType) && !objectType.IsAbstract)
return null;
return base.ResolveContractConverter(objectType);
}
}
And finally the usage:
foreach (var value in JsonConvert.DeserializeObject<List<Value>>(json))
{
if (value is StringValue)
{
string s = ((StringValue)value).Values;
}
}
I've been pondering about this for some time and I think I might just be missing an essential basic coding approach to resolve.
This is what I tried:
class DataSetCommonQuery
{
public string #operator;
public List<DataSetCommonQuery> rules = new List<DataSetCommonQuery>();
}
My expected JSON should be:
{
"payLoad": {
"DataSetCommonQuery": {
"operator": "AND",
"rules": [{
"field": "ENTITY.CIFNumber",
"condition": "<>",
"value": "3123"
},
{
"field": "ENTITY.Country",
"condition": "LIKE",
"value": "USA"
},
{
"operator": "OR",
"rules": [{
"field": "ENTITY.FYEMonth",
"condition": "=",
"value": "May"
},
{
"field": "STATEMENT.ProfitBeforeTax",
"condition": ">=",
"value": 123123
},
{
"field": "STATEMENT.NetSales",
"condition": "<=",
"value": 234234
},
{
"field": "STATEMENT.statementdatekey_",
"condition": "=",
"value": "2019-07-01 12:00:00"
}
]
}
]
}
}
}
You see there is again operator and rules inside the rules. Any thoughts on how should I be declaring the rules variable to be able to get the expected JSON. I am working to build the JSON on C#.
public static DataSetCommonQuery ConvertToJsonObject(string bracketContents)
{
DataSetCommonQuery commonQuery = new DataSetCommonQuery();
string[] operators = splitWithOperator(bracketContents);
commonQuery.#operator = ReturnOperator(bracketContents);
string output;
do
{
//bracketContesnts = getWhatsInsideBrackets(bracketContents);
for (int i = 0; i < splitWithOperator(bracketContents).Length; i++)
{
var jObject = new JObject();
if(!checkIfBracketsExists(operators[i]))
{
List<string> eachCondition = splitEachCondition(operators[i].Trim());
eachCondition.Add(operators[i].Replace(eachCondition[0], "").Replace(eachCondition[1], "").Trim());// Substring(operators1[i].IndexOf(eachCondition[0]), (operators1[i].IndexOf(eachCondition[1]) - operators1[i].IndexOf(eachCondition[0]))));
jObject.Add("field", eachCondition[0]);
jObject.Add("condition", eachCondition[2]);
jObject.Add("value", eachCondition[1]);
}
else if (checkIfBracketsExists(operators[i]))
{
ConvertToJsonObject(getWhatsInsideBrackets(operators[i]));
}
commonQuery.rules.Add(jObject); //THIS LINE SHOWS ERROR CAN NOT CONVERT JOBJECT TO DataSetCommonQuery
}
} while (checkIfBracketsExists(bracketContents));
return commonQuery;
}
Have you tried site converter like this Json2cssharp
This is really a good question, would suggest you to select Newtonsoft.json and do something like this
public partial class StackOverFlow
{
[JsonProperty("payLoad")]
public PayLoad PayLoad { get; set; }
}
public partial class PayLoad
{
[JsonProperty("DataSetCommonQuery")]
public DataSetCommonQuery DataSetCommonQuery { get; set; }
}
public partial class DataSetCommonQuery
{
[JsonProperty("operator")]
public string Operator { get; set; }
[JsonProperty("rules")]
public DataSetCommonQueryRule[] Rules { get; set; }
}
public partial class DataSetCommonQueryRule
{
[JsonProperty("field", NullValueHandling = NullValueHandling.Ignore)]
public string Field { get; set; }
[JsonProperty("condition", NullValueHandling = NullValueHandling.Ignore)]
public string Condition { get; set; }
[JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)]
public string Value { get; set; }
[JsonProperty("operator", NullValueHandling = NullValueHandling.Ignore)]
public string Operator { get; set; }
[JsonProperty("rules", NullValueHandling = NullValueHandling.Ignore)]
public RuleRule[] Rules { get; set; }
}
public partial class RuleRule
{
[JsonProperty("field")]
public string Field { get; set; }
[JsonProperty("condition")]
public string Condition { get; set; }
[JsonProperty("value")]
public Value Value { get; set; }
}
public partial struct Value
{
public long? Integer;
public string String;
public static implicit operator Value(long Integer) => new Value { Integer = Integer };
public static implicit operator Value(string String) => new Value { String = String };
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
ValueConverter.Singleton,
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
internal class ValueConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(Value) || t == typeof(Value?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.Integer:
var integerValue = serializer.Deserialize<long>(reader);
return new Value { Integer = integerValue };
case JsonToken.String:
case JsonToken.Date:
var stringValue = serializer.Deserialize<string>(reader);
return new Value { String = stringValue };
}
throw new Exception();
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
var value = (Value)untypedValue;
if (value.Integer != null)
{
serializer.Serialize(writer, value.Integer.Value);
return;
}
if (value.String != null)
{
serializer.Serialize(writer, value.String);
return;
}
throw new Exception();
}
public static readonly ValueConverter Singleton = new ValueConverter();
}
I want to deserialize (and afterwards serialize again) the following Json
{
"id": "12345",
"custom_fields": [
{
"definition": "field1",
"value": "stringvalue"
},
{
"definition": "field2",
"value": [ "arrayvalue1", "arrayvalue2" ]
},
{
"definition": "field3",
"value": {
"type": "user",
"id": "1245"
}
}
]
}
The type of the value if different (field1: string, field2: array, field3: an extra nested structure).
This what I have:
public class CustomField
{
public string Definition { get; set; }
public object Value { get; set; }
}
public class RootObject
{
public string Id { get; set; }
public List<CustomField> Custom_fields { get; set; }
}
The Value-field is defined as object, but this is a problem when serializing again, especially the nested structure for field3.
How can I solve this?
You could implement a custom converter for your Value property:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Test
{
public class Program
{
static void Main(string[] args)
{
var json =
#"{
""id"": ""12345"",
""custom_fields"": [
{
""definition"": ""field1"",
""value"": ""stringvalue""
}, {
""definition"": ""field2"",
""value"": [ ""arrayvalue1"", ""arrayvalue2"" ]
}, {
""definition"": ""field3"",
""value"": {
""type"": ""user"",
""id"": ""1245""
}
}
]
}";
var rootObject = JsonConvert.DeserializeObject<RootObject>(json);
Console.WriteLine(JsonConvert.SerializeObject(rootObject));
}
}
public class RootObject
{
[JsonProperty(PropertyName = "id")]
public string Id { get; set; }
[JsonProperty(PropertyName = "custom_fields")]
public List<CustomField> CustomFields { get; set; }
}
public class CustomField
{
[JsonProperty(PropertyName = "definition")]
public string Definition { get; set; }
[JsonProperty(PropertyName = "value")]
[JsonConverter(typeof(CustomValueConverter))]
public dynamic Value { get; set; }
}
public class CustomValueConverter : JsonConverter
{
// By returning false, we let the default `WriteJson` kick in
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return true;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return string.Empty;
}
if (reader.TokenType == JsonToken.String)
{
return serializer.Deserialize<string>(reader);
}
if (reader.TokenType == JsonToken.StartArray)
{
return serializer.Deserialize<string[]>(reader);
}
return serializer.Deserialize<Dictionary<string, string>>(reader);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
}