I have set up a new API endpoint (shown below) that expects a JObject in the request body. I am working towards a more complex example, but currently am attempting this with a simple boolean object.
However, when I attempt to hit the endpoint via Swagger, I get a http 400 error
The JSON value could not be converted to Newtonsoft.Json.Linq.JToken. Path: $.ShouldTakeAction | LineNumber: 1 | BytePositionInLine: 26
I've tried a few variations of the request body with no luck. What am I doing wrong?
[HttpPost]
[Route("CreateNodeWithAction")]
public ActionResult<string> CreateNodeWithAction([FromBody] JObject request)
{
if (request == null)
{
return BadRequest("Request body is null.");
}
bool shouldTakeAction = request.Value<bool>("ShouldTakeAction");
if (shouldTakeAction)
{
return Ok("Action taken.");
}
else
{
return Ok("No action taken.");
}
}
Sample of existing endpoint that I want to hit (the other was what I 'thought' I needed to create)
[HttpPost]
[Route("[action]")]
public IActionResult CreateMyObject(string field1, string field2, bool overrideSomething)
{ // Do Stuff }
2 Answers 2
The error is returned by ASP.NET Core itself. It can't parse the JSON text into a JObject. It doesn't even know what a JObject is. ASP.NET Core uses System.Text.Json, not JSON.NET. When it sees JObject it assumes that's your actual request DTO object and will try to deserialize the "ShouldTakeAction" attribute into a JObject.ShouldTakeAction property which doesn't exist.
ASP.NET Core also validates the incoming request, ensuring it matches the properties, types and validation defined on the request DTO. If the validation fails you get back a standardized 400 error response with a Problem Details body. That's what your screenshot shows.
APIs almost never accept open-ended requests. OpenAPI (whose old name was Swagger) is a way to specify what the API accepts and returns, not the UI you use for testing. ASP.NET Core can automatically generate the Open API schema document just from the request and response objects. The ASP.NET Core Web API project template already generates an Open API schema. You'll find the details in the docs, in the OpenAPI Overview
The simplest way to create an API that accepts the request you posted and returns a string is this:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseHttpsRedirection();
app.MapPost("/CreateNodeWithAction", (SomeAction it) =>
{
return it.ShouldTakeAction
? Results.Ok("Action Taken")
: Results.Ok("No Action Taken");
});
app.Run();
record SomeAction(bool ShouldTakeAction);
You can post to this using eg curl and get a response back.
curl -X 'POST' \
'http://localhost:5291/CreateNodeWithAction' \
-H 'accept: */*' \
-H 'Content-Type: application/json' \
-d '{
"shouldTakeAction": true
}'
The default Web API project template adds OpenAPI out of the box. All that's needed are the AddOpenAPI call that installs the OpenAPI middleware and MapOpenAPI which exposes the document at openapi/v1.json
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapPost("/CreateNodeWithAction", (SomeAction it) =>
{
return it.ShouldTakeAction
? Results.Ok("Action Taken")
: Results.Ok("No Action Taken");
});
app.Run();
This is the generated OpenAPI document :
{
"openapi": "3.1.1",
"info": {
"title": "actiontest | v1",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:5291/"
}
],
"paths": {
"/CreateNodeWithAction": {
"post": {
"tags": [
"actiontest"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SomeAction"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "OK"
}
}
}
}
},
"components": {
"schemas": {
"SomeAction": {
"required": [
"shouldTakeAction"
],
"type": "object",
"properties": {
"shouldTakeAction": {
"type": "boolean"
}
}
}
}
},
"tags": [
{
"name": "actiontest"
}
]
}
The Use OpenAPI documents page in the docs shows how you can use various packages to display the Open API document or call the API from the UI. Swagger UI, which is shown in the question's screenshots, is just one option
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/openapi/v1.json", "v1");
});
}
If you really want to accept "everything" you can use JsonObject instead of a DTO. In that case though the OpenAPI schema becomes useless (since everything is acceptable) and you'll have to manually handle everything - lookups, parsing, casting, validations. What if the request contains ShouldTakeAction or shouldtakeaction ? What if it's missing? What if it contains a string instead of a boolean?
And how will you tell clients what they should send?
//Really bad idea, don't use
app.MapPost("/CreateNodeWithAction", (JsonObject it) =>
{
return it["ShouldTakeAction"].GetValue<bool>()
? Results.Ok("Action Taken")
: Results.Ok("No Action Taken");
});
5 Comments
JsonObject but it's a bad idea. It's better to post a new question but people will ask why you want that and tell you that 1) that's a bad idea, you'll end up with a ton of code in the "dynamic" class to handle the various business cases and all the features you lose and 2) you already have a lot of dynamic and reusable features available through the Controller base class and middleware. So why do you need a "dynamic" component? There's almost certainly a better way, eg through custom middleware, or an injected service or something elseBased on information I got here and some trial, I currently have the below implementation (though this actually requires I create 'RequestBody' instances of any API I wish to use).
public class TestClass()
{
public void MyTest()
{
var data = new { projectName = "MyProject", nodeTypeName = "Basic" };
string uri = "http://localhost/Api/v1/CreateNode_BodyRequest";
var response = myApi.PostDataAsync(uri, data);
}
public async Task<string> PostDataAsync<T>(string uri, T data)
{
try
{
// Prepare the data to be sent in the request body
string jsonContent = Newtonsoft.Json.JsonConvert.SerializeObject(data);
HttpContent content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
// Send POST request
HttpResponseMessage response = await _client.PostAsync(uri, content);
response.EnsureSuccessStatusCode(); // Throws exception for non-success status codes.
string responseBody = await response.Content.ReadAsStringAsync();
return responseBody;
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Error posting data: {ex.Message}");
throw; // Re-throw the exception for handling in the calling code
}
}
}
public class ApiController()
{
// This results in a new API endpoint on our swagger page (not desired)
[HttpPost]
[Route("[action]_FromBody")]
public ActionResult<Guid> CreateNode([FromBody] MyExtension.CreateNodeRequest request)
{
return CreateNode(request.ProjectName, request.NodeType)
}
[HttpPost]
[Route("[action]")]
public ActioniResult<Guid> CreateNode(string projectName, string nodeTypeName)
{
// Do logic
// return Guid
}
}
public class MyExtension()
{
// Unique per API Endpoint, so I'll have to create a new one for each 'parameter' list
public class CreateNodeRequest()
{
string ProjectName { get; set; }
string NodeType { get; set; }
}
}
JObjectcontains on information. Might as well useobject. APIs just don't work that way. It's ASP.NET's job to serialize the request or response objects to JSON, not the application's. Try with proper classes instead ofJObject. Besides, ASP.NET Core uses System.Text.Json, not JSON.NETShouldTakeActionpropertyJObjectinstead of a simple custom class which defines the structure that the API expects? If there's a driving reason behind this, there's likely a better way to achieve that goal. Alternatively, if the reason was an attempt at simplicity, that attempt clearly introduced complexity instead.