In researching how to deserialize a TimeSpan using Newtonsoft's JSON.net I came across code in my current project that did not use Json.net. It used System.Text.Json.JsonSerializer and appeared to not fail on the operation of deserializing the TimeSpan property, as per the unit tests I was running.
Great I thought, .Net Core 3.1 has surpassed the historical issue of deserializing a TimeSpan and all is good. So fired up a test case in the latest version of Linqpad 6 (which uses .NET Core) to verify and to my chagrin it failed.
So the question is, can the TimeSpan be serialized/deserialized using either library (and if so how)… or is my test case below flawed in some respect?
Code
public class Project { public TimeSpan AverageScanTime { get; set; } }
Linqpad C# Code
var newP = new Project() { AverageScanTime = TimeSpan.FromHours(1) };
newP.Dump("New one");
var json = System.Text.Json.JsonSerializer.Serialize(newP);
json.Dump("JSON serialized");
System.Text.Json.JsonSerializer.Deserialize<Project>(json)
.Dump("JSON Deserialize");
Deserialize Failure
JsonSerializer for TimeSpan seem will added in Future (removed from .NET 6 milestone). You can trace this issue in Future milestone or this issue.
At this time, you can implement JsonTimeSpanConverter on your own. Or you can install Macross.Json.Extensions nuget package and follow the instruction to de/serializer.
An addition to the answer from Poy Chang
Swagger (Swashbuckle) also requires a configuration
services.AddSwaggerGen(options =>
{
options.MapType(typeof(TimeSpan), () => new OpenApiSchema
{
Type = "string",
Example = new OpenApiString("00:00:00")
});
});
TimeSpanConverter is available in .NET 6.0. So TimeSpan serialization/deserialization will work without custom converters out of the box.
Issue: https://github.com/dotnet/runtime/issues/29932
Implementation: https://github.com/dotnet/runtime/pull/54186
Related
I have this setup
Asp Core 3.1 API
Shared Lib with MyClass that is sent between API and client
Client App with Com classes
On the MyClass that is sent between them I have a field ComField that references a com class, this is only used on the client app and should not be (de)serialized, therefore I have it marked with [JsonIgnore]
class MyClass{
[JsonIgnore]
public ComThingy ComField {
get{// code here that throws the error when deserilaized on the API}
set{// code here}
}
}
When I write the API to accept the class like this, I get an error when the class is deserialized. The debugger throws the error while deserializing the MyClass, before it enters the method:
[HttpPost]
public async Task<ActionResult<MyClassReply>> Post([FromBody] MyClass myclass){
// code here
}
The API throws an exception that accessing the getter on MyClass throws an error (because that Com stuff isn't on the API).
If I deserialize manually it works fine, but then my swagger doesn't generate the whole API correctly.
[HttpPost]
public async Task<ActionResult<MyClassReply>> Post(){
// this works fine
var rdr = new StreamReader(Request.Body);
var mcj = await rdr.ReadToEndAsync();
var myclass = Newtonsoft.Json.JsonConvert.DeserializeObject<MyClass>(mcj);
// code here
}
So my question is: how come the ASP API builtin deserialization ignores the JsonIgnore attribute and still tries to deal with that property (throwing an error), and why does deserializing manually work as expected (ie ignore that property)? The default pipeline still uses NewtonSoft rght?
And how do I make the default deserialization work correctly?
Starting from ASP.NET Core 3.0, the default JSON serializer is System.Text.Json, and not Newtonsoft.Json. You need to call .AddNewtonsoftJson() in your Startup.cs to use it (see for example this answer).
Your issue might simply be that you're not using the proper JsonIgnore attribute. Both serializers have the same named attribute:
System.Text.Json.Serialization.JsonIgnoreAttribute
Newtonsoft.Json.JsonIgnoreAttribute
Maybe your using statement are importing the Newtonsoft.Json one instead of the System.Text.Json one?
I was accustomed to using Newtonsoft's JObject in my Asp.net web api's. I could add properties at will using:
JObject x = new JObject() { "myInt", 25 };
//or
x.Add("myInt", 25);
I ran into an error when trying to return a JObject from a ASP.Net core 3.1 web app.
System.NotSupportedException: The collection type 'Newtonsoft.Json.Linq.JObject' is not supported.
So NewtonSoft.Json is not supported in .Net core 3.1. So I looked at this article https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to but it seems that you can not add a property to a JsonDocument. This seems very odd and I thought I might be missing some information. So is there a way to add a property and value to a JsonDocument?
I normally use ShouldSerialize to exclude properties that have no data such as array but now, it does not appear to be triggered when I'm only using JSON serializer in .NET Core 3. It was being triggered when using NewtonSoft but I've removed it from my project since it no longer appears to be required.
For example:
private ICollection<UserDto> _users;
public ICollection<UserDto> Users
{
get => this._users ?? (this._users = new HashSet<UserDto>());
set => this._users = value;
}
public bool ShouldSerializeUsers()
{
return this._users?.Count > 0;
}
Any ideas why ShouldSerializeUsers is not being triggered?
I've seen other answers where you can use:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddJsonOptions(options => {
options.SerializerSettings.NullValueHandling =
NullValueHandling.Ignore;
});
}
But I'd like to know if there is another way to handle this as I'm not using .AddMvc
Thanks.
The reason that your ShouldSerialize is not triggered in ASP.NET Core 3.0 is that, in this and subsequent versions of ASP.NET, a different JSON serializer is being used by default, namely System.Text.Json.JsonSerializer. See:
Try the new System.Text.Json APIs.
Breaking changes to Microsoft.AspNetCore.App in 3.0 #325.
The future of JSON in .NET Core 3.0 #90.
Unfortunately as of .NET Core 3.1 this serializer does not support the ShouldSerializeXXX() pattern; if it did it would be somewhere in JsonSerializer.Write.HandleObject.cs -- but it's not. The following issues track requests for conditional serialization:
.net core 3.0 system.text.json option for ignoring property at runtime like newstonsoft DefaultContractResolver #42043.
System.Text.Json option to ignore default values in serialization & deserialization #779.
To restore ShouldSerialize functionality, you can revert back to using Newtonsoft as shown in this answer to Where did IMvcBuilder AddJsonOptions go in .Net Core 3.0? by poke, and also Add Newtonsoft.Json-based JSON format support:
Install Microsoft.AspNetCore.Mvc.NewtonsoftJson.
Then call AddNewtonsoftJson() in Startup.ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson();
}
It is possible in Net 5 to use conditional JsonIgnore. It does not give you full conditional option, but you can exclude null at least which I suppose is the most used case:
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? MyProperty { get; set; }
If one wants to allow for optional null in json, it is possible to use a custom Optional<T> struct that is similar to Nullable, like e.g. one from Roslyn. Then it's possible to have a value, null, or no field at all in the result JSON.
I have asp.net core 2.2 web Application. The application contains Command and Query Actions, Command Actions must return typed json response:
{
$type:"A.B.Customer, x.dll ",
id: 11231,
name: "Erwin .."
}
Query Actions must return non typed response:
{
id: 34234,
name: "Erwin .. "
}
We can choose one outputformatter for Json responses at startup.
services.AddMvc().AddJsonOptions(o =>
{
SerializerSettings.TypeNameHandling=Newtonsoft.Json.TypeNameHandling.Objects
// SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.None
}
So How can I change output formatter for same response type (application/json) by action?
There are multiple ways to do that.
One would be to directly call JSON.NET Methods and pass your settings to it.
JsonSerializerSettings settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects
};
return base.Content(JsonConvert.SerializeObject(query, settings), "application/json");
Alternatively, return a JsonResult
JsonSerializerSettings settings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects
};
return new JsonResult(query, settings);
Be careful with the second example. In .NET Core 3.0, JSON.NET isn't hard-wired into ASP.NET Core anymore and ASP.NET Core ships w/o JSON.NET and uses the new System.Text.Json classes base don span.
In ASP.NET Core 2.x, JsonResult accepts JsonSerializerSettings as second parameter.
From ASP.NET Core 3.x, JsonResult accepts object and depending on the serializer used, a different type is expected.
This means, if you use the second example in your code, it will (at first) break, when migrating to ASP.NET Core 3.0, since it has no dependencies on JSON.NET anymore. You can easily add JSON.NET Back to it by adding the Newtonsoft.Json package to your project and add
servics.AddNewtonsoftJson();
in your ConfigureServices methods, to use JSON.NET again. However, if in future, you ever decide to move away from JSON.NET and use System.Text.Json or any other Json serializer, you have to change all places where that's used.
Feel free to change that to an extension method, create your own action result class inheriting from JsonResult etc.
public class TypelessJsonResult : JsonResult
{
public TypelessJsonResult(object value) : base(value)
{
SerializerSettings.TypeNameHandling = TypeNameHandling.None;
}
}
and reduce your controller's action code to
return new TypelessJsonResult(query);
I'm trying to improve the default RestSharp serialization by using Json.net library. In order to customize the serialization you have to implement ISerializer interface:
public class LowerCaseSerializer : ISerializer{
public LowerCaseSerializer(){
ContentType = "application/json";
}
public string Serialize(object obj){
var settings = new JsonSerializerSettings{
ContractResolver = new LowerCaseResolver()
};
return JsonConvert.SerializeObject(obj, Formatting.None, settings);
}
string ISerializer.RootElement { get; set; }
string ISerializer.Namespace { get; set; }
string ISerializer.DateFormat { get; set; }
public string ContentType { get; set; }
}
As you see I'm also extending the ContractResolver. This is the actual code that does the lowercasing:
public class LowerCaseResolver : DefaultContractResolver{
protected override string ResolvePropertyName(string propertyName){
return propertyName.ToLower();
}
}
Once all this is setup I can use it with RestSharp:
var request = new RestRequest(url, method);
if (ShouldAddBody(method)){
request.JsonSerializer = new LowerCaseSerializer();
request.AddObject(body);
}
var response = client.Execute<T>(request);
Everything works, except the properties are not in lower case. When debugging the debuger goes into the Constructor of the serializers, but it's method is never called. When I tried exactly the same for deserializations (IDeserialize interface, which attaches to the client) the method for lower casing was called for each property.
What I have also tried:
request.RequestFormat = DataFormat.Json; // no change
// this correctly lower cases the properties
var json = new LowerCaseSerializer().Serialize(body);
// wrong request sent to the server, it tells me that some property is missing
request.AddBody(json);
// the exact same thing with this
request.AddParameter("application/json", json, ParameterType.RequestBody);
The thing I noticed with the last two: if I have lower case properties and let RestSharp serializes then the request has 5 parameters (one for each property). If I add it via upper two methods it has only one property and that's the whole json.
I check the RestSharp issues to no avail. Any suggestions?
Update:
This very strange:
forked the RestSharp, installed Json.net, works fine
copied RestRequest class from RestSharp fork, pasted to my application, used it, works fine
started new project, installed RestSharp and Json.net via Package manager, works fine
Then removed all packages from my main application, redownloaded, doesn't work. Kinda giving up on this.
Update 2:
Debugging through the forked version I noticed this: The RequestFormat has to be Dataformat.Json or it will use Xml serializer. But that doesn't fix the problem. I tried setting both (there are only two) serializers to null:
request.JsonSerializer = null;
request.XmlSerializer = null;
In the new project I did this causes NullReferrenceException as expected. But in the old one nothing happens. I tried renaming the variables, making another variable of the same type, but nothing fixes is. It just seems that in the project I have the RestRequest class is somehow bugged.
I also added a new project to the solution. And even there the code works. So it's just one project that has the problem.
Since you can't reproduce it in a new project, there must be something different going on in this particular project, that's causing the issues, you're describing.
A couple of things you could try (in no particular order):
Check that you're using the exact same version of the library in both projects (the one that works and the one that doesn't): package version and target platform (net4, net35...).
Delete the packages folder of your non-working project so that NuGet will be forced to re-download all the packages.
Lookup the exact path to the referenced library in Visual Studio Properties window when you have RestSharp from References node selected. Do a binary compare between the libraries referenced by the working and the non-working project.
Unfortunately there's no symbol package for RestSharp on SymbolSource, so you can't directly debug RestSharp in your non-working project. You could use Reflector.NET VSPro if you have the license or haven't used the trial before.
Move parts of your non-working project to the working one until it stops working.
EDIT:
Looking at the source code of RestRequest, AddObject doesn't seem to use the JsonSerializer you are setting. Have you tried using AddBody instead?