Web API/JsonMediaTypeFormatter accepts Invalid JSON and passes null argument to action - asp.net-web-api

I have the following model:
public class Resource
{
[DataMember(IsRequired = true)]
[Required]
public bool IsPublic { get; set; }
[DataMember(IsRequired = true)]
[Required]
public ResourceKey ResourceKey { get; set; }
}
public class ResourceKey
{
[StringLength(50, MinimumLength = 1)]
[Required]
public string SystemId { get; set; }
[StringLength(50, MinimumLength = 1)]
[Required]
public string SystemDataIdType { get; set; }
[StringLength(50, MinimumLength = 1)]
[Required]
public string SystemEntityType { get; set; }
[StringLength(50, MinimumLength = 1)]
[Required]
public string SystemDataId { get; set; }
}
I have the following action method signature:
public HttpResponseMessage PostResource(Resource resource)
I send the following request with JSON in the body (an intentionally invalid value for property "IsPublic"):
Request Method:POST
Host: localhost:63307
Connection: keep-alive
Content-Length: 477
User-Agent: Mozilla/5.0 (Windows NT 6.0) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.97 Safari/537.22
Origin: chrome-extension://hgmloofddffdnphfgcellkdfbfbjeloo
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
{
"IsPublic": invalidvalue,
"ResourceKey":{
"SystemId": "asdf",
"SystemDataIdType": "int",
"SystemDataId": "Lorem ipsum",
"SystemEntityType":"EntityType"
},
}
This is invalid JSON - run it through JSONLint and it tells you:
Parse error on line 2:
{ "IsPublic": invalidvalue,
.................^ Expecting 'STRING', 'NUMBER', 'NULL', 'TRUE', 'FALSE', '{', '['
The ModelState.IsValid property is 'true' - WHY???
Also, instead of throwing a validation error, the formatter seems to give up on deserializing and simply passes the 'resource' argument to the action method as null!
Note that this also happens if I put in an invalid value for other properties, e.g. substituting:
"SystemId": notAnObjectOrLiteralOrArray
However, if I send the following JSON with a special undefined value for the "SystemId" property:
{
"IsPublic": true,
ResourceKey:{
"SystemId": undefined,
"SystemDataIdType": "int",
"SystemDataId": "Lorem ipsum",
"SystemEntityType":"EntityType"
},
}
Then I get the following, reasonable, exception thrown:
Exception Type: Newtonsoft.Json.JsonReaderException
Message: "Error reading string. Unexpected token: Undefined. Path 'ResourceKey.SystemId', line 4, position 24."
Stack Trace: " at Newtonsoft.Json.JsonReader.ReadAsStringInternal()
at Newtonsoft.Json.JsonTextReader.ReadAsString()
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter)
at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)"
SO: what is going on in the Newtonsoft.Json library which results in what seems like partial JSON Validation???
PS: It is possible to post JSON name/value pairs to the Web API without enclosing the names in quotes...
{
IsPublic: true,
ResourceKey:{
SystemId: "123",
SystemDataIdType: "int",
SystemDataId: "Lorem ipsum",
SystemEntityType:"EntityType"
},
}
This is also invalid JSON!

OK - so it appears that part of the problem was caused by my own doing.
I had two filters on the controller:
Checks whether there are any null action parameters being passed to an action method and if so, returns a "400 Bad Request" response stipulating that the parameter cannot be null.
A ModelState inspection filter which checked the Errors of the ModelState and if any are found, return them in a "400 Bad Request" response.
The mistake I made was to put the null argument filter before the model state checking filter.
After Model Binding, the serialization would fail correctly for the first JSON example, and would put the relevant serialization exception in ModelState and the action argument would remain null, rightfully so.
However, since the first filter was checking for null arguments and then returning a "404 Bad Request" response, the ModelState filter never kicked in...
Hence it seemed that validation was not taking place, when in fact it was, but the results were being ignored!
IMPORTANT: Serialization exceptions that happen during Model Binding are placed in the 'Exception' property of the ModelState KeyValue pair Value...NOT in the ErrorMessage property!
To help others with this distinction, here is my ModelValidationFilterAttribute:
public class ModelValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (actionContext.ModelState.IsValid) return;
// Return the validation errors in the response body.
var errors = new Dictionary<string, IEnumerable<string>>();
foreach (KeyValuePair<string, ModelState> keyValue in actionContext.ModelState)
{
var modelErrors = keyValue.Value.Errors.Where(e => e.ErrorMessage != string.Empty).Select(e => e.ErrorMessage).ToList();
if (modelErrors.Count > 0)
errors[keyValue.Key] = modelErrors;
// Add details of any Serialization exceptions as well
var modelExceptions = keyValue.Value.Errors.Where(e => e.Exception != null).Select(e => e.Exception.Message).ToList();
if (modelExceptions.Count > 0)
errors[keyValue.Key + "_exception"] = modelExceptions;
}
actionContext.Response =
actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
}
}
And here is the action method, with the filters in the correct order:
[ModelValidationFilter]
[ActionArgNotNullFilter]
public HttpResponseMessage PostResource(Resource resource)
So now, the following JSON results in:
{
"IsPublic": invalidvalue,
"ResourceKey":{
"SystemId": "asdf",
"SystemDataIdType": "int",
"SystemDataId": "Lorem ipsum",
"SystemEntityType":"EntityType"
},
}
{
"resource.IsPublic_exception": [(2)
"Unexpected character encountered while parsing value: i. Path 'IsPublic', line 2, position 21.",
"Unexpected character encountered while parsing value: i. Path 'IsPublic', line 2, position 21."
]-
}
However, all of this does not explain why invalid JSON is still parsed by the JsonMediaTypeFormatter e.g. it does not require that names be strings.

More of a workaround than an answer, but I was able to get this to work using the workaround posted at http://aspnetwebstack.codeplex.com/workitem/609. Basically, instead of having your Post method's signature take a Resource instance, make it take no parameters and then use JSon.Net (or a new instance of JsonMediaTypeFormatter) to do the deserialization.
public void Post()
{
var json = Request.Content.ReadAsStringAsync().Result;
var resource = Newtonsoft.Json.JsonConvert.DeserializeObject<Resource>(json);
//Important world saving work going on here
}

Related

passing json in json to spring controller

I am trying to pass json object to spring controller and I manage to do that, but value of one property is in json and I think that I have problem because of it. But there is no other way to pass that data. Code is below,
data class:
#Entity
data class Section(
#Id
#GeneratedValue
val id: Long = 0L,
val name: String = "",
var text: String,
#ManyToOne
var notebook: Notebook
)
Controller code:
#PutMapping("/sections/{id}")
fun updateSection(#RequestBody section: Section, #PathVariable id: Long): Section =
sectionRepository.findById(id).map {
it.text = section.text
it.notebook = section.notebook
sectionRepository.save(it)
}.orElseThrow { SectionNotFoundException(id) }
javascript sending post to api:
function updateApi(data) {
axios.put(MAIN_URL + 'sections/' + data.id, {
data
})
.then(showChangesSaved())
.catch(ShowErrorSync());
}
function saveSection() {
var data = JSON.parse(window.sessionStorage.getItem("curr-section"));
data.text = JSON.stringify(element.editor).toString();
updateApi(data);
}
I get error like this:
2020-11-18 15:06:24.052 WARN 16172 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class org.dn.model.Section] value failed for JSON property text due to missing (therefore NULL) value for creator parameter text which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class org.dn.model.Section] value failed for JSON property text due to missing (therefore NULL) value for creator parameter text which is a non-nullable type
at [Source: (PushbackInputStream); line: 1, column: 375] (through reference chain: org.dn.model.Section["text"])]
so text in element.editor is JSON formatted string and I need to pass it as it is to controller. Is there any way to do that? I tried searching, but I can't find json in json help...
Whole project is available on github
What does your json looks like? If I check out your project and run the following two tests:
one with Section as an object as request body
one with Section as json
Both will succeed. So the problem might lie in your JSON:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class HttpRequestTest {
#LocalServerPort
private val port = 0
#Autowired
private val restTemplate: TestRestTemplate? = null
#Test
fun sectionAsObject() {
val section = Section(0L, "2L", "text", Notebook(1L, "1", "2"))
assertThat(restTemplate!!.put("http://localhost:$port/sections/123", section
)).isNotNull
}
#Test
fun sectionAsJson() {
val sectionAsJson = """
{
"id": 0,
"name": "aName",
"text": "aText",
"noteBook": {
"id": 0,
"name": "aName",
"desc": "2"
}
}
""".trimIndent()
assertThat(restTemplate!!.put("http://localhost:$port/sections/123", sectionAsJson
)).isNotNull
}
}
BTW: it is not a pretty good habit to expose your database ids, which is considered to be a security risk as it exposes your database layer. Instead, you might want to use a functional unique key ;)

Capturing ONLY sent fields in the Request body in Spring Boot PATCH rest API

I am trying to implement a PATCH API as follows:
#PatchMapping("/student)
public ResponseEntity<StudentDTO> patchStudent(#RequestBody StudentDTO studentDTO)
throws URISyntaxException {
...
}
Here StudentDTO is as follows:
class StudentDTO {
String name,
String rollNum,
String grade,
String id,
...
}
Here in PATCH API, user may send any number of fields including id as follows:
Request 1:
{
id: 1,
name: "Test"
}
Request 2:
{
id:1,
name: "Test",
rollNumber : "123456"
}
Request 3:
{
id:1,
name: "Test",
rollNumber : "123456",
grade : null. //NOTE: user may send null as well for a field in request body
}
I a not getting how should I capture only those fields in the request body while patching the data in the backend?

How I can return my custom json file instead of default json file that generates spring boot?

I have a rest controller for authorization:
#RestController
class AuthController {
#PostMapping("/sign-up")
fun signUp(#RequestBody signUpRequest: SignUpRequest): ResponseEntity<String> {
some code here..
}
}
The signUp method gets SignUpRequest model as a request body. SignUpRequest model is:
enum class Role {
#JsonProperty("Student")
STUDENT,
#JsonProperty("Tutor")
TUTOR
}
data class SignUpRequest(
val role: Role,
val email: String,
val password: String
)
When I make /sign-up post request with JSON:
{
"role": "asdf",
"email": "",
"password": ""
}
It returns me an answer that were generated by spring boot:
{
"timestamp": "2020-02-12T05:45:42.387+0000",
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: Cannot deserialize value of type `foo.bar.xyz.model.Role` from String \"asdf\": not one of the values accepted for Enum class: [Student, Tutor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `foo.bar.xyz.model.Role` from String \"asdf\": not one of the values accepted for Enum class: [Student, Tutor]\n at [Source: (PushbackInputStream); line: 3, column: 10] (through reference chain: foo.bar.xyz.model.SignUpRequest[\"role\"])",
"path": "/sign-up"
}
Question is: How I can return my custom JSON instead of that default generated JSON?
I want to return my custom JSON, like:
{
"result": "Invalid user data are given",
"errors": [
{
"fieldName": "ROLE",
"text": "Given role does not exist"
},
{
"fieldName": "EMAIL",
"text": "EMAIL is empty"
}
]
}
I suggest you to create ErrorContrller that generates custom json map as response. Then when you will catch an error in sign-up method, call ErrorContrllers method.
You can find info from this link
Finally I found out a solution. You should create a class that annotates #ControllerAdvice, and make a method that annotates #ExceptionHandler.
#ControllerAdvice
class HttpMessageNotReadableExceptionController {
#ExceptionHandler(HttpMessageNotReadableException::class)
#ResponseBody
#ResponseStatus(HttpStatus.BAD_REQUEST)
fun handleException(
exception: HttpMessageNotReadableException
): PostSignUpResponseError {
val errors = mutableListOf<PostSignUpResponseErrorItem>()
errors.add(
PostSignUpResponseErrorItem(
fieldNamePost = "Role",
text = "Given role does not exist"
)
)
return PostSignUpResponseError(
result = "Invalid user data are given",
errors = errors
)
}
}
where PostSignUpResponseErrorItem and PostSignUpResponseError are:
data class PostSignUpResponseError(
val result: String,
val errors: List<PostSignUpResponseErrorItem>
)
class PostSignUpResponseErrorItem(
val fieldNamePost: PostSignUpRequestFieldName,
val text: String
)
Anyway, I still don't know how to attach this thing to a certain PostMapping method.

JSON How can I make an object translate to Name.First instead of Name[First]?

I'm working on a website and I have a controller that takes in a lot of data, including a Name field:
public Name name { get; set; }
defined as follows:
public class Name
{
[RegularExpression(#"^[a-zA-ZÀ-ÿ\s-'.*]*$")]
public string First { get; set; }
[RegularExpression(#"^[a-zA-ZÀ-ÿ\s-'.*]*$")]
public string Last { get; set; }
public string DatabaseName { get; set; }
}
I've been using an MVC page that passes data through this controller, and it works. All the fields on this page are MVC HTML helpers, so I have an input box:
#HTML.TextBoxFor(m => m.Name.First)
Using Fiddler, I'm able to see that the data is being passed as Name.First : "john", which is correct, and this returns the results correctly.
However, I have another page that I am not able to use HTML helpers on. For most of the fields, I'm able to get the data correctly using JSON and an asynchronous AJAX call:
var queryModel = {
SoundEx: $("#Soundex").val(),
ExactSearch: $(".exact").val(),
Name : {First : $('.col-sm-5 > .form-control[placeholder="First and Middle Names"]').val(),
Last: $('.col-sm-5 > .form-control[placeholder="Last Name"]').val()},
FromYear: $('.form-control[id="yearFrom"]').val(),
ToYear: $('.form-control[id="yearTo"]').val(),
SelectedType: $("#RecordDropdown").val(),
Location: $('.form-control[placeholder="Enter Location"]').val(),
Favorites: $("#Favorite").val(),
Free: $("#Free").val(),
Images: $("#Images").val(),
Category: $("#category").val(),
Database: $("#database").val(),
Keywords: $('.form-control[placeholder="Enter terms or words"]').val(),
FamilyMembers: [{ Relationship: $("#Type1").val(), Firstname: $("#First1").val(), LastName: $("#Last1").val() },
{ Relationship: $("#Type2").val(), Firstname: $("#First2").val(), LastName: $("#Last2").val() },
{ Relationship: $("#Type3").val(), Firstname: $("#First3").val(), LastName: $("#Last3").val() }],
PageNum: 1,
PageSize: 20,
newSearch: 1,
SortBy: "Relevance"
}
$(resultsDiv).html("<h3>Searching...</h3>");
resultsDiv.show();
$.ajax({
url: "http://nehgs.mvc/SearchResults/Results",
type: 'post',
dataType: 'text/html',
success: function (info) {
$(resultsDiv).html(info.responseText);
},
data: queryModel
});
All of my search fields work except for the name and relationship fields, i.e. whatever is typed in them is correctly searched for in my database, and the results returned are correct. However, when the name or relationship fields are filled out, they don't get searched for; I still get data returned, but those fields are treated as though they are blank.
In Fiddler, I can see that these fields are being passed as
Name[First]
and
Relationships[0][Firstname]
Whereas in the page where those fields are working, they are being passed as
Name.First
and
Relationships[0].Firstname
This is the only thing I can think of that would cause this issue. How can I make the JSON pass is as Name.First instead of Name[First]?
I solved this by using the toDictionary() plugin available on this page:
http://erraticdev.blogspot.com/2010/12/sending-complex-json-objects-to-aspnet.html

WebApi 2.1 Model validation works locally, but does not show missing fields when run on the server

We are using WebApi v2.1 and validating ModelState via a filter applied in the WebApiConfig class.
Fields specified as required are not listed on the error message when we run on the server (Win Server 2008R2), but they work perfectly when we run locally (on IISExpress).
The request is correctly rejected locally and on the server, but the server response does not show the missing fields.
For Example:
Given a Local request that lacks the required abbreviation and issuerName fields, the response shows as expected:
{
"message": "The request is invalid.",
"modelState": {
"value": [
"Required property 'abbreviation' not found in JSON. Path '', line 18, position 2.",
"Required property 'issuerName' not found in JSON. Path '', line 18, position 2."
]
}
When the same request is sent to the server, the response shows:
{
"message": "The request is invalid.",
"modelState": {
"value": [
"An error has occurred.",
"An error has occurred."
]
}
}
Our filter is the following:
public class ValidateModelStateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if (!actionContext.ModelState.IsValid)
{
actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
Our data model class is decorated with the DataContract attribute, and the required fields are attributed like so:
[DataMember(IsRequired=true)]
public string IssuerName
The server is more restrictive about sending errors down ot the client. Try setting the IncludeErrorDetails flag on your httpconfiguration to verify that this is the underlying issue.
In general though turning this flag on is not the best idea, and you will want to serialize the errors down differently.
For more info:
http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx

Resources