graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

Precise JSON Variable Parsing and Handling

Open lingxiao-microsoft opened this issue 4 months ago • 14 comments

Product

Hot Chocolate

Is your feature request related to a problem?

Hello Community,

Thanks for your great work.

We'd like to reach out to and see if there are some gaps in feature support or we're not doing it correctly that caused the issue.

Problem

How to handle variables defined on client side and pass it correctly to HC for execution?

  • Example, we have a front end that takes in the Variables that client side defines as a JSON to pass to backend.
  • On backend side, we have a logic to parse the variables based on the type of the JSON element and then build the Variables dictionary and pass to HC for execution for filter.

Example of our code snippet


public Dictionary<string, object> GetGraphQLVariables()
        {
            Dictionary<string, object> keyValuePairs = new Dictionary<string, object>();

            if (Variables == null || Variables.Count == 0)
            {
                return keyValuePairs;
            }

            foreach ((string variableName, JsonElement variable) in Variables)
            {
                keyValuePairs.Add(variableName, ConvertJsonElementToObject(variable));
            }

            return keyValuePairs;
        }


private static object ConvertJsonElementToObject(JsonElement jsonElement)
        {
            switch (jsonElement.ValueKind)
            {
                case JsonValueKind.String:
                    return jsonElement.ToString();

                case JsonValueKind.Number:
                    // Handle different number types (int, double, etc.)
                    if (jsonElement.TryGetInt16(out short shortValue))
                    {
                        return shortValue;
                    }
                    else if (jsonElement.TryGetInt32(out int intValue))
                    {
                        return intValue;
                    }
                    else if (jsonElement.TryGetDouble(out double floatValue))
                    {
                        return floatValue;
                    }
                    else if (jsonElement.TryGetInt64(out long longValue))
                    {
                        return longValue;
                    }
                    else
                    {
                        throw new InvalidArtifactMetadataRequestException($"Invalid {nameof(jsonElement.ValueKind)}.{nameof(Variables)} has invalid variable of type {jsonElement.ValueKind}");
                    }

                case JsonValueKind.True or JsonValueKind.False:
                    return jsonElement.GetBoolean();
                case JsonValueKind.Null:
                    return null;
                case JsonValueKind.Array:
                    var list = new List<object>();
                    foreach (var item in jsonElement.EnumerateArray())
                    {
                        list.Add(ConvertJsonElementToObject(item));
                    }

                    return list;
                case JsonValueKind.Object:
                    var dictionary = new Dictionary<string, object>();
                    foreach (JsonProperty property in jsonElement.EnumerateObject())
                    {
                        dictionary[property.Name] = ConvertJsonElementToObject(property.Value);
                    }

                    return dictionary;
                default:
                    throw new Error;
            }
        }

This will have below problems

  1. When a column is DateTime, and client side defines a datetime in string, e.g. "2024-01-01", if we directly pass the string value into the Variable dictionary - Dictionary<string, object>. HC will error out as DateTime Column can't parse the string automatically.
  2. But if we always proactively TryParse DateTime from the string, then we're making an assumption that the Column type should be DateTime. Cuz if the column type happens to be String, then String can't parse the DateTime type automatically.
  3. Similarly for the Number types, I can see a problem that we're converting to int,long,double based on value provided in the variable instead of the actual column type.

Ask

  1. What should be the correct way of handling this?
  2. Is there an option we can directly pass the Raw string from JSON to HC and let it handle the problem for us instead of us trying to check on the column type?

Background about our Setup

  • We're directing building a IOperationRequest and send it to IRequestExecutor to execute the query directly through C# class instead of running a GraphQL server.
            IOperationRequest request = OperationRequestBuilder.New()
                .SetDocument(query)
                .....
                .SetVariableValues(variables)
                .Build();
IRequestExecutor requestExecutor = await requestExecutorBuilder.AddGraphQLServer()
            .ConfigureSchema((sB) =>
            {
                SchemaGenerator.GenerateSchemaBuilder(sB, schema);
            })

            .DisableIntrospection(!allowIntrosepctionQuery)
            .AddMaxExecutionDepthRule(GraphQLConstants.GQLMaxQueryDepth)
            .UseTimeout()
            .ModifyRequestOptions(o =>
            {
                o.ExecutionTimeout = GraphQLConstants.HotChocolateTimeoutLimit;
            })
            .UseDefaultPipeline()
            .UseCostAnalyzer()
            .ModifyCostOptions(o =>
            {
              
                o.DefaultResolverCost = GraphQLConstants.GQLResolverComplexity;
                o.MaxFieldCost = GraphQLConstants.GQLMaximumAllowedComplexity;
            })
        ........
            .TryAddTypeInterceptor(fetchMiddlewareFromContext ? new DabResolverTypeInterceptor() : new DabResolverTypeInterceptor(executionHelper, queryMiddlewareDefinition, mutationMiddlewareDefinition)) // TODO: Migrate to always using method that fetches the middleware from context
            .BuildRequestExecutorAsync(cancellationToken: ct); 

The solution you'd like

  • HC provided a way that can take in variables that are in raw string from JSON
  • Or if HC can suggest a better way that we should use to resolve the issue on our side

lingxiao-microsoft avatar Sep 11 '25 17:09 lingxiao-microsoft

Hi @lingxiao-microsoft

You can likely use the JsonValueParser, and pass the variable values as value nodes, which HC will handle.

Here is an example:

https://github.com/ChilliCream/graphql-platform/blob/ae8b99cbcb17a23b3c6a1661c50a86a3bd911cd9/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Handlers/CallToolHandler.cs#L41-L48

glen-84 avatar Sep 12 '25 07:09 glen-84

@lingxiao-microsoft the JsonValueParser should work for you guys. Its in the HotChocolate language package and reads the json and emits GraphQL literals.

michaelstaib avatar Sep 15 '25 15:09 michaelstaib

Thank you both for the reply, will let the team know and try it out the solution!

lingxiao-microsoft avatar Sep 22 '25 17:09 lingxiao-microsoft

Hi @glen-84 / @michaelstaib

It looks like this JsonValueParser will parse from the string directly and convert it c# classes like FloatValueNode and etc.

I wonder if the actual data type mapping logic is handled somewhere else later when we supply the actual schema. Would appreciate a code link pointer to that logic if that is the case.

Thanks!

lingxiao-microsoft avatar Sep 23 '25 04:09 lingxiao-microsoft

What are you trying to do? The actual data logic in GraphQL is happening depending on various things ... the variable coercion is for instance happening later in the pipeline.

The logic where you put things in here is actually dealing with different kinds of raw data. Typically we use GraphQL literals in this case StringValue, IntValue, EnumValue, FloatValue, BooleanValue.

The DateTime in this case would have to be an RFC3339 formatted string. We made this strict to align with the DateTime scalar spec.

https://www.graphql-scalars.com/date-time/

You can however register the scalar with relaxed settings and then the string you provided should be abled to be coerced.

.AddType(new DateTimeType(disableFormatCheck: true))

IRequestExecutor requestExecutor = await requestExecutorBuilder.AddGraphQLServer()
            .ConfigureSchema((sB) =>
            {
                SchemaGenerator.GenerateSchemaBuilder(sB, schema);
            })

            .DisableIntrospection(!allowIntrosepctionQuery)
            .AddMaxExecutionDepthRule(GraphQLConstants.GQLMaxQueryDepth)
            .AddType(new DateTimeType(disableFormatCheck: true))
            .UseTimeout()
            .ModifyRequestOptions(o =>
            {
                o.ExecutionTimeout = GraphQLConstants.HotChocolateTimeoutLimit;
            })
            .UseDefaultPipeline()
            .UseCostAnalyzer()
            .ModifyCostOptions(o =>
            {
              
                o.DefaultResolverCost = GraphQLConstants.GQLResolverComplexity;
                o.MaxFieldCost = GraphQLConstants.GQLMaximumAllowedComplexity;
            })
        ........
            .TryAddTypeInterceptor(fetchMiddlewareFromContext ? new DabResolverTypeInterceptor() : new DabResolverTypeInterceptor(executionHelper, queryMiddlewareDefinition, mutationMiddlewareDefinition)) // TODO: Migrate to always using method that fetches the middleware from context
            .BuildRequestExecutorAsync(cancellationToken: ct); 

michaelstaib avatar Sep 23 '25 12:09 michaelstaib

I'm trying to see by using the suggested parser, will we be able to solve below problems -

Examples

  1. Customer has a column type as DateTime, then we should try to map the variable to DateTime.
  2. Customer has column type as String, and then we should try to map the variable value to string instead.
  3. Similarly for float, int, we should try to map/convert based on the actual data type.

So my previous question was, if we use JsonValueParser to parse JSON, which will convert to FloatValueNode and etc, then once we pass the Node type into HC, will HC handle the data mapping problem above for us?

Hope that makes sense! Thanks

lingxiao-microsoft avatar Sep 23 '25 23:09 lingxiao-microsoft

So my previous question was, if we use JsonValueParser to parse JSON, which will convert to FloatValueNode and etc, then once we pass the Node type into HC, will HC handle the data mapping problem above for us?

Yes, basically what is happening is your are building up the context for the GraphQL request. Once its being executed, the execution pipeline will validate the operation, coerce the variables and execute the operation.

Customer has a column type as DateTime, then we should try to map the variable to DateTime.

No, just conform to the format of the scalar or relax the DateTimeType to allow all possible DateTime formats. This really depends a bit on your use-case. The variable coercion is what is doing this already.

michaelstaib avatar Sep 24 '25 07:09 michaelstaib

Can we close this issue or do you have more questions about the variable coercion?

michaelstaib avatar Sep 28 '25 10:09 michaelstaib

We can close this issue, thank you so much for the information shared!

lingxiao-microsoft avatar Sep 29 '25 19:09 lingxiao-microsoft

@glen-84 / @michaelstaib , Is the JsonValueParser available from HC16 because from the namespace using hotchocolate.langugae the JsonValueParser still has compilation error. So wondering if this is available in HC16 since we are using HC15

Alekhya-Polavarapu avatar Oct 14 '25 20:10 Alekhya-Polavarapu

Yes, JsonValueParser is new in v16.

glen-84 avatar Oct 15 '25 07:10 glen-84

@glen-84 , thanks for your response, is there any alternative to use for HC15?

Alekhya-Polavarapu avatar Oct 15 '25 16:10 Alekhya-Polavarapu

I think we can port it back to v15 ... I will have a look at it tonight.

michaelstaib avatar Oct 17 '25 10:10 michaelstaib

Thankyou so much @michaelstaib , do post us the updates , so that we can move accordingly!

Alekhya-Polavarapu avatar Oct 17 '25 16:10 Alekhya-Polavarapu