Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

“The JSON value could not be converted to System.String” when attempting to call controller endpoint

I've been trying to create a simple API, I manage to make the Get work just fine but whenever I try to work with Post or Put I can't get it to work.
I'm trying to post/put a JSON and getting it as a string in my controller.
I'm using Postman and Insomnia to test (I precise I turned of SSL verification for both since I run in local).
Here is my controller:

[Route("backoffice/[controller]")]
[ApiController]
public class AddQuestionController : ControllerBase
{
    private IQuestionRepository _questionRepository;

    public AddQuestionController(IQuestionRepository questionRepository)
    {
        _questionRepository = questionRepository ?? throw new ArgumentNullException(nameof(questionRepository));

    }

    [ProducesResponseType((int)System.Net.HttpStatusCode.OK)]
    [HttpPost]
    public async Task<ActionResult> AddQuestion([FromBody] string question)
    {
        Question q = JsonConvert.DeserializeObject<Question>(question);
        await Task.Run(() => _questionRepository.InsertOne(q));
        return Ok();
    }
}

postman

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "traceId": "|a0b79872-4e41e975d19e251e.",
    "errors": {
        "$": [
            "The JSON value could not be converted to System.String. Path: $ | LineNumber: 0 | BytePositionInLine: 1."
        ]
    }
}

So then I thought it's because the Json format in postman. But then I tried the text format and this happened:

{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.13",
    "title": "Unsupported Media Type",
    "status": 415,
    "traceId": "|a0b79873-4e41e975d19e251e."
}

And every time it doesn't even reach the first line of my controller. Can someone tell me what I did wrong here? Is it my controller? Is it my way of using Postman?

like image 504
Platypus Avatar asked Mar 28 '20 18:03

Platypus


People also ask

Why JSON value could not be converted to string?

System.Text.Json.JsonException: ‘The JSON value could not be converted to System.String This issue is more because of the new .NET /ASP.NET Core 3.1 framework serializer which has removed the dependency on JSON.NET and uses it’s own JSON serializer i.e ‘ System.Text.Json ‘.

What is system JSON jsonexception?

System.Text.Json doesn’t deserialize non-string values like Int, Boolean and other primitives into string properties. System.Text.Json.JsonException: ‘The JSON value could not be converted to System.String

Why JSON serializer is not working in ASP core?

This issue is more because of the new .NET /ASP.NET Core 3.1 framework serializer which has removed the dependency on JSON.NET and uses its own JSON serializer i.e ‘ System.Text.Json ‘. There are known limitations in the System.Text.Json serializer which are as per specification and design.

What is jsonexception Int32 in Java?

System.Text.Json doesn’t deserialize non-string values like Int, Boolean and other primitives into string properties. Any non-string value conversion produce JsonException with the following message, System.Text.Json.JsonException: ‘The JSON value could not be converted to System.Int32.


Video Answer


2 Answers

The model binder is unable to map/bind the sent data to the controller parameters

Your action expects a simple string from the request body

public async Task<ActionResult> AddQuestion([FromBody] string question)

But you sent a complex object

{ "test" : "test" }

You might have gotten a match if the property name(s) had matched

For example

{ "question" : "test" }

Since the model binder will take property names into consideration when matching parameters.

if you want to receive a raw string then you need to send a valid raw JSON string

"{ \"test\": \"test \"}"

That is properly escaped.

Another options is to use a complex object for the parameter

class Question  {
    public string test { get; set; }
    //...other properties
}

that matches the expected data

public async Task<ActionResult> AddQuestion([FromBody] Question question) {
    string value = question.test;

    //...
}

The model binder will bind the data and pass it to the action parameter(s).

Reference Model Binding in ASP.NET Core

like image 112
Nkosi Avatar answered Oct 17 '22 18:10

Nkosi


Thanks @Nkosi for identifying the issue and providing the article Model Binding in ASP.NET Core.

Since I spent a lot of time going through outdated examples on how to call an API, here's my code for reference (as of Sep-2020):

On the API project, I used the [BindProperty] attribute on the properties of the model class.

    // ASP.NET CORE API - C# model

    using Microsoft.AspNetCore.Mvc;
    using System;
    
    namespace MyTestAPI.Models
    {
        public partial class MyTest
        {
            [BindProperty]
            public int TestId { get; set; }
            [BindProperty]
            public string Message { get; set; }
            [BindProperty]
            public Guid? CreatedBy { get; set; }
            [BindProperty]
            public DateTime Timestamp { get; set; }
        }
    }

On the API controller, the mytest class is automatically deserialized because of the property attributes of the MyTest model class:

// ASP.NET CORE API - C# controller

using Dapper;
using HangVue.API.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using MyTestAPI.Models;

namespace HangVue.API.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TestController : ControllerBase
    {     

       [HttpPost]
       public void Post([FromBody] MyTest mytest)
       {

           var parameters = new DynamicParameters();
           parameters.Add("@pMessage", mytest.Message, System.Data.DbType.String, System.Data.ParameterDirection.Input);
           parameters.Add("@CreatedBy", mytest.CreatedBy.ToString(), System.Data.DbType.String, System.Data.ParameterDirection.Input);

           string sql = "[dbo].[uspTest]";

           using (var conn = new System.Data.SqlClient.SqlConnection(*** SQL_conn_string_goes_here ***))
           {
             var affectedRows = conn.Query(sql, parameters, commandType: System.Data.CommandType.StoredProcedure);
           }
       }
    }  
}

On the client side, I'm using Xamarin.Forms with RestSharp to invoke my API. The AccessToken is required because I'm using Azure AD B2C authentication.

// Xamarin.Forms - C# Client (iOS + Android)

using Microsoft.Identity.Client;
using RestSharp;
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;

namespace YourApp.Services
{
    public static class TestServices
    {   

       public async static Task<string> NewTest(int iTestId, string sMessage, Guid? gCreatedBy, DateTime dTimestamp)
       {
          try
          {
             var mytest = new Models.MyTest 
             {
                TestId = iTestId,
                Message = sMessage,
                CreatedBy = gCreatedBy,
                Timestamp = dTimestamp
             };

             // Client --- API end-point example:  https://yourAPIname.azurewebsites.net/
             RestSharp.RestClient client = new RestClient(*** https://Your_API_base_end_point_goes_here ***);
            
             // Request 
             RestSharp.RestRequest request = new RestSharp.RestRequest("api/test", RestSharp.Method.POST, RestSharp.DataFormat.Json);
             request.AddParameter("Authorization", "Bearer " + *** Your_AccessToken_goes_here ***, RestSharp.ParameterType.HttpHeader);
             request.AddHeader("Content-Type","application/json; CHARSET=UTF-8");
             request.AddHeader("Accept", "application/json");
             request.AddJsonBody(mytest);

             // Invoke
             RestSharp.IRestResponse response = await client.ExecuteAsync(request);

             if (response.StatusCode == System.Net.HttpStatusCode.OK)
             {
                *** do something ***    
                return *** a string *** ;
             }
             else
             {
                *** do something ***
                return *** a string *** ;
             }
          }
          catch (Exception ex)
          {
             *** do something ***
          }
       }
    }
}
like image 25
Richard H Avatar answered Oct 17 '22 17:10

Richard H