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

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


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 ***
          }
       }
    }
}