graphql-dotnet / graphql-dotnet

GraphQL for .NET

Home Page:https://graphql-dotnet.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

.net 8 Variable 'myObject' is invalid. Unable to parse input as a 'myObject' type. Did you provide a List or Scalar value accidentally?

Will-Alger opened this issue · comments

Summary

Similar to issue raised in #3582

experiencing the error:
'areaInput' is invalid. Unable to parse input as a 'areaInput' type. Did you provide a List or Scalar value accidentally?

Recently upgraded to .net 8, and graphql-dotnet v7, inspected migration guides but still running into issues somewhere in setup.

Stepping through the source code, it seems its failing because it expects the variables to be a dictionary<string, object>, but instead its a JObject still.

Wondering if Model Binding is partly the root cause. Do I need to explicitly capture the Httpcontext and deserialize using GraphQLSerializer?

Relevant information

.net 8
using GraphQL.NewtonsoftJson;

startup.cs

services.AddSingleton<IDocumentExecuter, DocumentExecuter>();
services.AddScoped<IDataLoaderContextAccessor, DataLoaderContextAccessor>();
services.AddScoped<IServiceProvider>(
    s => new FuncServiceProvider(s.GetRequiredService)
);
services.AddScoped<ISchema, NavigationSchema>();

services.AddGraphQL(b => b
    .AddNewtonsoftJson()
    .AddSchema<NavigationSchema>()
    .AddGraphTypes()
    .AddErrorInfoProvider(o => o.ExposeExceptionDetails = true)
    .AddUserContextBuilder(ctx => new GraphQLUserContext { User = ctx.User })
);

//authentication
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<GraphQLAuth.IAuthorizationEvaluator, AuthorizationEvaluator>();
services.AddTransient<GraphQLVal.IValidationRule, AuthorizationValidationRule>();

GraphQLController

using GraphQLRequest = GraphQL.Transport.GraphQLRequest;
using GraphQL.NewtonsoftJson;

 [HttpPost]
 public async Task<IActionResult> Post([FromBody] GraphQLRequest request, [FromServices] IEnumerable<IValidationRule> validationRules)
 {
     var r = await ExecuteGraphQLRequest(request, validationRules);
     return r;
 }

private async Task<IActionResult> ExecuteGraphQLRequest(GraphQLRequest request, IEnumerable<IValidationRule> validationRules)
{
    var result = await _documentExecuter.ExecuteAsync(s =>
    {
        s.Schema = _schema;
        s.Query = request.Query;
        s.Variables = request.Variables;
        s.ValidationRules = validationRules;
        s.OperationName = request.OperationName;
        s.RequestServices = HttpContext.RequestServices;
        s.ThrowOnUnhandledException = true;
        s.User = HttpContext.User;
        s.UserContext = new GraphQLUserContext
        {
            User = HttpContext.User,
        };
        s.CancellationToken = HttpContext.RequestAborted;
    }).ConfigureAwait(false);

    if (result.Errors?.Count > 0)
    {
        if (result.Errors.Select(e => e.Code).Contains("authorization"))
        {
            return Unauthorized();
        }
        else
        {
            return BadRequest(result);
        }
    }
    return new ExecutionResultActionResult(result);
}

example query:

export const addAreaQuery = gql`
mutation addArea($areaInput: AreaInput!) {
  createArea(area: $areaInput) {
    id
    parentId
    displayName
  ...
  }
}

example data payload:

{
  "operationName": "addArea",
  "variables": {
    "areaInput": {
      "displayName": "test",
      ...
    }
  },
  "query": "mutation addArea($areaInput: AreaInput!) {\n  createArea(area: $areaInput) {\n    id\n    parentId\n    displayName ...
}

Are you open to using GraphQL.Server.Transports.AspNetCore rather than your own ASP.NET Core middleware / controller action?

Can you list all of the graphql nuget packages you have installed?

Can you give a very brief explanation of how you are handling authentication?

Do you use subscriptions?

Do you use subscriptions?

I do not use subscriptions.

Are you open to using GraphQL.Server.Transports.AspNetCore rather than your own ASP.NET Core middleware / controller action?

Going to be testing this out to see it if is a viable solution for me. This might be the best option.

Can you give a very brief explanation of how you are handling authentication?

Without too much detail, using a policy-based authentication system.

Can you list all of the graphql nuget packages you have installed?

  • GraphQL 7.8.0
  • GraphQL.Authorization 7.0.0
  • GraphQL.DataLoader 7.8.0
  • GraphQL.NewtonsoftJson 7.8.0
  • GraphQL.Server.Transports.AspNetCore 7.7.1
  • GraphQL.Server.Ui.Playground 7.7.1

I'd suggest following the setup instructions in the readme of the server repo for setup, replacing your controller action entirely.

https://github.com/graphql-dotnet/server

While I'd be happy to walk you through it, the readme contains all the information you need and should be well documented. You can set it up as middleware, the default configuration, or as a MVC controller action by following these instructions. The readme also contains additional information regarding endpoint routing, CORS, compression, and so on.

I would like to note that your current controller implementation returns 400 for any error. The GraphQL.NET Server middleware by default will return 200 for any request that has begun execution, whether successful or not, and 400 for any request that fails validation. If this is an issue, you can derive from the middleware and override the WriteJsonResponseAsync method to set the status code in the event of any error.

In addition, I'd personally recommend replacing GraphQL.Authorization with the authorization rule included with the server repo. This is not necessary, but the older library doesn't contain support for the newest authorization features, and I think there may be some implementation bugs when dealing with fragments. The newer authorization rule within the server repo is very well tested and is more tightly integrated with the ASP.NET Core authentication pipeline (for better or worse). See instructions here.

All these links just bring you back to the server readme. I'd recommend skimming through the entire readme so you can have an idea of what the capabilities are; then you can decide on an implementation strategy. There are several example projects in the repo as well, mentioned at the bottom of the readme, which you can use as a reference to properly configure your Startup.cs.

Finally, if you switch from GraphQL.NewtonsoftJson to GraphQL.SystemTextJson, you may see some slight performance improvement as System.Text.Json supports asynchronous serialization and deserialization, whereas Newtonsoft.Json does not. As long as you have no custom scalars which rely on NSJ or STJ specifically, it likely doesn't matter which one is used. STJ is also included with .NET and does not require an external dependency.

And if you prefer to retain your existing controller action, then going back to your original question:

Wondering if Model Binding is partly the root cause. Do I need to explicitly capture the Httpcontext and deserialize using GraphQLSerializer

Yes that's probably true. The GraphQL.NET engine will expect an Inputs class which is a dictionary containing values, lists or other dictionaries, not JObject instances. Also, the current version of GraphQL.NET is quite strict about the data types received during parsing of scalars. For instance, if NSJ deserializes a date string to a DateTime, but you're using a DateTimeOffsetScalar, it's not going to work; GraphQL.NET is going to expect a string (or would allow a DateTimeOffset). And the string it expects is going to need to meet ISO specifications; otherwise it will be rejected. You can review the migration notes across each major version of GraphQL.NET for other changes, these are just a couple items to be aware of.

In any case, I'd take a look at the sample controller implementation and go from there.

I also looked further at your original code. Probably you just need to register the InputsJsonConverter and GraphQLRequestJsonConverter with the ASP.NET Core model binding serialization system and it might work. Something like this perhaps:

    services.AddControllers()
        .AddNewtonsoftJson(options =>
        {
            // Adding custom converters
            options.SerializerSettings.Converters.Add(new InputsJsonConverter());
            options.SerializerSettings.Converters.Add(new GraphQLRequestJsonConverter());
        });
}

I have not tested it, but maybe that's all that is required.

Have made pretty good progress with GraphQL.Server.Transports.AspNetCore solution. Only one niche problem I've been stuck on.

Without making trying to make any breaking changes, this API receives requests that sometimes have uppercase "Query" or uppercase "Variables" as keys. I was able to handle this with my custom controller but no longer with this solution.

Is there any way to make the deserialization case-insensitive for the keys? Is my only option to create my own JsonConverter?

looking at the keys defined here:

private const string QUERY_KEY = "query";

and here

private const string VARIABLES_KEY = "variables";

Thanks!

I probably don't have much to offer besides what you've already found out. This is by design to meet spec. I'd say the most 'correct' answer for your situation would be to use your own JsonConverter, or perhaps just remove the one that was installed, with a case-insensitive name converter. However, this is a bit of a pain with Newtonsoft.Json because even if you don't have to write a JsonConverter, you'd have to write your own ContractResolver.

With System.Text.Json, this might just work fine:

var serializer = new GraphQLSerializer(o => {
    // remove the installed JsonConverter
    o.Converters.Remove(o.Converters.First(c => c is GraphQLRequestJsonConverter));

    // then:
    o.PropertyNameCaseInsensitive = true;
    // or this:
    //o.Converters.Add(new MyGraphQLRequestJsonConverter());
});
services.AddSingleton<IGraphQLTextSerializer>(serializer);
services.AddSingleton<IGraphQLSerializer>(serializer);
// be sure to remove .AddSystemTextJson(); within .AddGraphQL builder

Note that this is still going to be a case-sensitive match for variable names and so on.

While it's possible to override GraphQLHttpMiddleware.ReadPostContentAsync to parse and deserialize the form/json data in any way you want, I would not recommend that, as you'd need to copy a lot of code that may change. Replacing or removing the JsonConverter should have very little impact on future versions and carries throughout everywhere that GraphQLRequest is deserialized, whether it is a standard or batch request via POST, operation over websocket, etc.

The documentation I was looking to in particular was seen here: https://graphql-dotnet.github.io/docs/guides/serialization/

image

My particular case is the second one, where I thought Newtonsoftjson would be case insensitive by default, but I suppose its conflicting with the GraphQL.Newtonsoftjson which is not case-insensitive?

Sorry for the confusion, just trying to clarify.

stepping through with a debugger, I can see that my request object looks something like this:

{{ "Query": "query GetUsers($userId: ID!) {users(userId: $userId) { id, firstName, lastName, } }", "Variables": { "userId": "<someId>" } }}

The documentation is incorrect, if used with GraphQLRequest. For the custom Request class shown on documentation page it would work the same, since GraphQLRequestJsonConverter would not be used. The documentation should be updated.

When the GraphQLRequest class was added to GraphQL.NET for v5, the GraphQLRequestJsonConverter was included which requires case-sensitive match for the property names regardless of the serializer's settings.

@Shane32 with your comment, I think I now have a working solution.

Copied the System.text.json version of GraphQlRequestJsonConverter from the repo here

Changes the switch case in that code to use
switch (key.ToCamelCase())

defined my converter in startup.cs as:

 var customSerializer = new GraphQLSerializer(o =>
 {
     o.Converters.Add(new CustomGraphQLRequestJsonConverter());
     o.ReferenceHandler = ReferenceHandler.IgnoreCycles;
 });

and added it to my AddGraphQL() function with
.AddSerializer(customSerializer)

ultimately this solved me serialization problems and my case insensitive issue.

Nice! Thanks for sharing the solution also!

Close issue?

yep! thanks!