In trying to troubleshoot some code, I looked to the log where I am capturing any unhandled exceptions.
In my log function, I have the following line of code:
data = JsonConvert.DeserializeObject(exception)
Part of the value of data reads, \"Message\":null. When I debug and look at the exception, the Message property of the exception has a value (which accurately describes the issue). If I try to convert a different exception to JSON using the line above, I see the value of the Message property in the resulting JSON.
What am I missing?
This looks like a bug in AutoMapperMappingException. Json.NET supports ISerializable, and the base class System.Exception implements this interface. As can be seen from the reference source, the base class version of GetObjectData simply adds the base field value not the property value to the serialization stream:
info.AddValue("Message", _message, typeof(String));
Any subclass of Exception that holds additional data above and beyond the base class must needs override GetObjectData and serialize its own data. For instance, here is the implementation of ArgumentOutOfRangeException:
public override void GetObjectData(SerializationInfo info, StreamingContext context) {
if (info==null) {
throw new ArgumentNullException("info");
}
Contract.EndContractBlock();
base.GetObjectData(info, context);
info.AddValue("ActualValue", m_actualValue, typeof(Object));
}
Now, from its source code, AutoMapperMappingException overrides Exception.Message to output additional data. And in fact, the base message might be null:
public override string Message
{
get
{
string message = null;
var newLine = Environment.NewLine;
if (Context != null)
{
message = _message + newLine + newLine + "Mapping types:";
message += newLine + string.Format("{0} -> {1}", Context.SourceType.Name, Context.DestinationType.Name);
message += newLine + string.Format("{0} -> {1}", Context.SourceType.FullName, Context.DestinationType.FullName);
var destPath = GetDestPath(Context);
message += newLine + newLine + "Destination path:" + newLine + destPath;
message += newLine + newLine + "Source value:" + newLine + (Context.SourceValue ?? "(null)");
return message;
}
if (_message != null)
{
message = _message;
}
message = (message == null ? null : message + newLine) + base.Message;
return message;
}
}
It does not, however, override GetObjectData(), which is the bug.
To work around the problem, you could write your own custom JsonConverter and serialize the message property instead of the base field:
public class ExceptionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(AutoMapperMappingException).IsAssignableFrom(objectType);
}
public override bool CanRead { get { return false; } }
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException();
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var exception = (Exception)value;
var obj = JObject.FromObject(exception);
obj["Message"] = exception.Message;
obj.WriteTo(writer);
}
}
This will at least ensure the message is present. However, the ResolutionContext property of AutoMapperMappingException still won't be serialized, so the resulting deserialized exception will have lost some data.
Alternatively, you could try serializing and deserializing the ResolutionContext with Json.NET's default serialization and adding it as a property in the converter. Not sure if it would work, but it might. Or try to report an issue.
(Or, just log the ToString() output of the exception, that's usually good enough.)
Related
We are trying to resolve issues related to a security scan. It is considered a vulnerability to expose any information about underlying classes. The scanner is sending invalid data to this endpoint:
#PostMapping(value = "/accountKey", params = "update")
public String accountKeyUpdate(#Valid #ModelAttribute("accountKeyForm") AccountKeyForm key, BindingResult bindingResult, Authentication authentication)
The invalid input looks like this, where "description" is a valid key in the entity, but adding "[]" to the end of the property name in the POST data is causing the parsing error:
description[]:
The server returns the following:
{
"timestamp": "2018-04-20T14:28:36.653Z",
"status": 500,
"error": "Internal Server Error",
"message": "Invalid property 'description[]' of bean class
[com.imsweb.seerapi.account.AccountKeyForm]: Property referenced in indexed property path 'description[]' is neither an array nor a List nor a Map; returned value was []",
"path": "/accountKey/"
}
This is what appears in the log:
org.springframework.beans.InvalidPropertyException: Invalid property 'description[]' of bean class [com.imsweb.seerapi.account.AccountKeyForm]: Property referenced in indexed property path 'description[]' is neither an array nor a List nor a Map; returned value was []
at org.springframework.beans.AbstractNestablePropertyAccessor.processKeyedProperty(AbstractNestablePropertyAccessor.java:375) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:275) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:266) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:97) ~[spring-beans-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:839) ~[spring-context-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.validation.DataBinder.doBind(DataBinder.java:735) ~[spring-context-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:197) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.bind.ServletRequestDataBinder.bind(ServletRequestDataBinder.java:107) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.bindRequestParameters(ServletModelAttributeMethodProcessor.java:157) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:153) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:124) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:161) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:131) ~[spring-web-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:877) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:783) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:974) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:877) ~[spring-webmvc-5.0.5.RELEASE.jar:5.0.5.RELEASE]
The issue is that I cannot find a way to gracefully handle the invalid input. It looks like it happens when the #ModelAttribute is converting the POST body into an AccountKeyForm. That is before it gets inside the controller method. I would prefer to handle the error and just forward them to another page. Alternatively if the message said
"message": "Invalid property 'description[]'"
That would be fine as well.
UPDATE:
I can trap that specific exception using an #ExceptionHandler:
#ControllerAdvice
public class WebControllerAdvice {
#ExceptionHandler(InvalidPropertyException.class)
public String handleBadPropertyException() {
return "error";
}
}
That means I will just get a generic message. That will not pick up other types of exceptions that may fall through the cracks. Is there a better way?
UPDATE:
Here is the entity class. It is a simple bean with two properties.
public class AccountKeyForm {
private String _apiKey;
private String _description;
public AccountKeyForm() {
}
public AccountKeyForm(String apiKey) {
_apiKey = apiKey;
}
public AccountKeyForm(String apiKey, String description) {
_apiKey = apiKey;
_description = description;
}
public String getApiKey() {
return _apiKey;
}
public void setApiKey(String apiKey) {
_apiKey = apiKey;
}
#Size(max = 256)
public String getDescription() {
return _description;
}
public void setDescription(String description) {
_description = description;
}
}
The solution for this is indeed to wrap the exception using the ControllerAdvice, but you need to tweak the response to your needs.
So, instead of returning a String, you should return a full ResponseEntity with a httpStatus and body. The body should be populated with an ErrorResponse where you can define your domain error code if you have something like that and your custom message.
Something like the code below should work.
#ControllerAdvice
public class WebControllerAdvice {
#ExceptionHandler(InvalidPropertyException.class)
public ResponseEntity<ErrorResponse> handle(InvalidPropertyException e) {
return ResponseEntity.status(httpStatus)
.body(new ErrorResponse(errorCode, message));
}
}
public class ErrorResponse {
private final String code;
private final String message;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}
}
It's clearing saying the following
Property referenced in indexed property path 'description[]' is neither an array nor a List nor a Map; returned value was []
Which means that the description field which is being sent from the request is of type array/List/map, so accordingly,you have to change the Model class AccountKeyForm description
from private String _description; to private List<String> _description; or
private Map<String> _description; you will need to figure out what collection type is being sent :)
or you'll have to modify how the request is being sent and ensure that it send only String type and not of List/Map type
The former is an easier solution.
Hope it helps :)
I have a custom JsonConverter which handles the creation of derived types during deserialization, in most cases this works as expected.
The situation where I have an issue is, when there are referenced objects in the json structure.
Is it possible to rely on the default deserialization when we detect a reference? What should the ReadJson method return?
In the sample below we return null in case of a reference.
if (reader.TokenType == JsonToken.Null) return null;
var jObject = JObject.Load(reader);
JToken token;
if (jObject.TryGetValue("$ref", out token))
{
return null;
}
Or must we implement a custom ReferenceResolver as the default one can't be used in the converter (only internal use)?
Any suggestions are welcome.
After some extra testing, I found the solution myself. When I first was trying using the default ReferenceResolver I got an exception saying "The DefaultReferenceResolver can only be used internally.". This pointed my in the wrong direction, you can use the DefaultReferenceResolver in your converter but I was calling it the wrong way...
Solution:
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
var jObject = JObject.Load(reader);
string id = (string)jObject["$ref"];
if (id != null)
{
return serializer.ReferenceResolver.ResolveReference(serializer, id);
}
// Custom instance creation comes here
}
In the following code, I serialize an object using Json.Net. This Json has type names embedded. I then change one of the type names to induce an error (this is a test, I am dealing with a real issue in an existing project). When I deserialize the Json, I expect to get an object back that has a null value for the property with the fiddled type name. Instead the serializer craps out and returns null. Are my expectations correct? Can I change the settings somehow so that I will get a non-null object for my root object? Note that the second error that I get suggests that there is a bug in the serializer.
static public class JsonTest
{
static public void Test()
{
// Create test object
A a = new A
{
MyTest = new MyTest(),
};
// Serialize it.
string json = JsonConvert.SerializeObject(a, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto
});
// Fiddle class name to induce error
json = json.Replace("+MyTest", "+MyTest2");
// Before: {"MyTest":{"$type":"<Namespace>.JsonTest+MyTest, <Assembly>"}}
// After: {"MyTest":{"$type":"<Namespace>.JsonTest+MyTest2, <Assembly>"}}
// Deserialize
A a2 = JsonConvert.DeserializeObject<A>(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
Error = (object sender, ErrorEventArgs e) =>
{
e.ErrorContext.Handled = true; // Should have only one error: the unrecognized Type
}
});
// A second error occurs: Error = {Newtonsoft.Json.JsonSerializationException: Additional text found in JSON string after finishing deserializing object....
// a2 is null
}
public class A
{
public ITest MyTest { get; set; }
}
public interface ITest { }
public class MyTest : ITest { }
}
Update
This issue has been fixed in Json.NET 10.0.2 in this submission.
Original Answer
This looks to be a bug in Json.NET. If I set JsonSerializerSettings.MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead then the problem goes away:
// Deserialize
A a2 = JsonConvert.DeserializeObject<A>(json, new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
Error = (object sender, Newtonsoft.Json.Serialization.ErrorEventArgs e) =>
{
Debug.WriteLine(e.ErrorContext.Path);
e.ErrorContext.Handled = true; // Should have only one error: the unrecognized Type
}
});
Debug.Assert(a2 != null); // No assert.
However, it should not be necessary to turn on this setting, which enables reading metadata properties including "$type" located anywhere in a JSON object, rather than just as the first property. Most likely it coincidentally fixes the bug since it requires pre-loading the entire JSON object before beginning deserialization.
You could report an issue if you want.
Debugging a bit, the problem seems to be that, because the inner MyTest object cannot be constructed, the exception is caught and handled by JsonSerializerInternalReader.PopulateObject() while populating the outer object A. Because of this, the JsonReader does not get advanced past the inner, nested object, leaving the reader and serializer in an inconsistent state. This accounts for the second exception and the eventual Additional text found in JSON string after finishing deserializing object. Path '' exception.
I have an Asp.Net WebApi controller with the following action:
public void Post([FromBody]object value)
Now, sometimes this value parameter is sent as a String containing a ISO8601-formatted date, and sometimes as a DateTime. The data is sent in JSON format.
For each of those two options I have to do different things, so I need to make that distinction, but I always end up with a DateTime value.
I am aware of the fact that my source DateTime values are being serialized to JSON as strings in ISO8601 format when they are sent to the above action (I am using an HttpClient for the actual sending), and hence my action cannot differentiate between the two.
My question is whether it is possible to include some kind of type hint to the sent values, in order to tell my WebApi endpoint which specific type is being sent (and enforce that type on the deserialized value).
Solved this eventually by implementing a custom JsonConverter, which serializes string values along with a type hint (similar to Newtonsoft.Json's MetadataPropertyHandling setting in JsonSerializerSettings).
Here is the implementation which does the trick:
public class DateStringTypeConservingConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType.IsAssignableFrom(typeof(string));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var token = JToken.ReadFrom(reader);
var typeMetadata = token["$type"];
if (typeMetadata?.Value<string>() == typeof(string).FullName)
{
return token.Value<string>("$value");
}
// Default behavior
return serializer.Deserialize(reader, objectType);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value is string)
{
WriteJsonStringWithType(writer, value as string);
}
else
{
// Default behavior
serializer.Serialize(writer, value);
}
}
// Write the given string to the given JSON writer with type info:
// { $type: "System.String", $value: "<string content>" }
private void WriteJsonStringWithType(JsonWriter writer, string value)
{
writer.WriteStartObject();
writer.WritePropertyName("$type");
writer.WriteValue(typeof(string).FullName);
writer.WritePropertyName("$value");
writer.WriteValue(value);
writer.WriteEndObject();
}
}
And a little usage example:
static void Main(string[] args)
{
var dateString = new Wrapper
{
Value = "2017-01-08T21:24:48.114Z"
};
var converters = new JsonConverter[] { new DateStringTypeConservingConverter() };
var serializedDateString = JsonConvert.SerializeObject(dateString, new JsonSerializerSettings
{
Converters = converters
});
var deserializedDateString =
JsonConvert.DeserializeObject<Wrapper>(serializedDateString, converters);
// Output: System.String
Console.WriteLine($"The type of deserialized value is: { deserializedDateString.Value.GetType() }");
Console.ReadKey();
}
class Wrapper
{
public object Value { get; set; }
}
I've written a REST endpoint with ASP.NET Web API 2 that receives dates. The backend system has no concept of UTC times or DST, the dates stored in the database are just the UK date and time.
The website feeding the endpoint, however, is including UTC offset values in the data sent to the end point (e.g. a date looks like "1939-01-08T00:00:00+01:00"). This happens when a summer date is entered in the winter or vice-versa (because of the adjustment for DST).
Is it possible for me to set the JSON deserializer to completely ignore those UTC offset values? I've looked at the docs here and the examples here and tried all of the different enum options, but none of them are giving me the behaviour I'm looking for.
If the date parsing isn't working the way you want, you can turn it off and use a custom JsonConverter to handle the dates instead.
Here is a converter that should do the job:
class OffsetIgnoringDateConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(DateTime) || objectType == typeof(DateTime?));
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
string rawDate = (string)reader.Value;
if (rawDate == null) return existingValue;
if (rawDate.Length > 19) rawDate = rawDate.Substring(0, 19);
DateTime date = DateTime.ParseExact(rawDate, "yyyy'-'MM'-'dd'T'HH':'mm':'ss",
CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal);
return date;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
To install this converter into the Web API pipeline, add the following to your Application_Start() method in Global.asax:
var config = GlobalConfiguration.Configuration;
var jsonSettings = config.Formatters.JsonFormatter.SerializerSettings;
jsonSettings.DateParseHandling = DateParseHandling.None;
jsonSettings.Converters.Add(new OffsetIgnoringDateConverter());
Here is a demo (console app): https://dotnetfiddle.net/kt9pFl