Precise JSON Variable Parsing and Handling
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
- 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. - 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.
- 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
- What should be the correct way of handling this?
- 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
IOperationRequestand send it toIRequestExecutorto 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
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
@lingxiao-microsoft the JsonValueParser should work for you guys. Its in the HotChocolate language package and reads the json and emits GraphQL literals.
Thank you both for the reply, will let the team know and try it out the solution!
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!
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);
I'm trying to see by using the suggested parser, will we be able to solve below problems -
Examples
- Customer has a column type as DateTime, then we should try to map the variable to DateTime.
- Customer has column type as String, and then we should try to map the variable value to string instead.
- 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
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.
Can we close this issue or do you have more questions about the variable coercion?
We can close this issue, thank you so much for the information shared!
@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
Yes, JsonValueParser is new in v16.
@glen-84 , thanks for your response, is there any alternative to use for HC15?
I think we can port it back to v15 ... I will have a look at it tonight.
Thankyou so much @michaelstaib , do post us the updates , so that we can move accordingly!