graphql-dotnet/graphql-dotnet

How to avoid transforming into AST json-style value (ScalarGraphType) after migrate v4 to v5

MakaBuka opened this issue · 8 comments

Hello guys!

I've just started upgrading my project from 4.6 to 5.x version of GraphQL.Net.

I have custom a quite simple JsonGraphType that looks like the next one:

    internal class JsonGraphType : ScalarGraphType
    {
        public JsonGraphType() => this.Name = "JSON";

        public override object ParseLiteral(IValue value) => JsonSerializer.Serialize(value.Value);

        public override object ParseValue(object value) {
            var jsonString = JsonSerializer.Serialize(value);
            return jsonString.FromJson<object>();
        }

        public override object Serialize(object value) => this.ParseValue(value);
    }

That allows me to process, for example, the next queries (for further parsing in my code):

query { 
   contacts(filter:{id_nin:[1,2,3,4,5,6,7]}) {id name}
 }

--
Based on migration docs - the contract of ParseLiteral has been changed:
public override object ParseLiteral(GraphQLValue value)

where GraphQLValue has no Value property, actually it's representing AST.

I'm a little bit stuck with migration because in the latest version of lib there's no value itself as it was before.

Could you tell me - if is it possible to get full value ({id_nin:[1,2,3,4,5,6,7]}) without representing it as AST?
Or maybe I did something wrong?

Thanks!
Dmytro.

This is how we handle Json. We need it parsed as JObject. You may need to adjust it a bit if you need it in a different form

public class JsonGraphType : ScalarGraphType
{
    public JsonGraphType()
    {
        Name = "JSON";
        Description = "The Json scalar type represents JSON (JavaScript Object Notation)";
    }

    public override object Serialize(object value) => value switch
    {
        null => null,
        IDictionary valueDict => valueDict,
        string valueString => string.IsNullOrEmpty(valueString) ? string.Empty : JsonConvert.DeserializeObject(valueString),
        JObject valueJObject => valueJObject,
        _ => ThrowSerializationError(value)
    };

    public override object ParseValue(object value) => value switch
    {
        null => null,
        JObject => value,
        IDictionary => JObject.FromObject(value),
        _ => ThrowValueConversionError(value)
    };

    public override object ParseLiteral(GraphQLValue value) => value switch
    {
        GraphQLNullValue _ => null,
        GraphQLStringValue stringValue => JsonConvert.DeserializeObject((string)stringValue.Value),
        _ => ThrowLiteralConversionError(value)
    };
}

This might work:

    internal class JsonGraphType : AnyScalarGraphType
    {
        public JsonGraphType() => this.Name = "JSON";

        public override object? ParseLiteral(GraphQLValue value)
        {
            var jsonString = JsonSerializer.Serialize(base.ParseLiteral(value));
            return jsonString.FromJson<object>();
        }

        public override object ParseValue(object value)
        {
            var jsonString = JsonSerializer.Serialize(value);
            return jsonString.FromJson<object>();
        }

        public override object Serialize(object value) => this.ParseValue(value);
    }

This uses the internal method ParseAnyLiteral indirectly through the AnyScalarGraphType base class, which contains the functionality you are looking for.

@gao-artur Can you review #3257 ? AnyScalarGraphType was specifically designed for GraphQL federation, and is missing some functionality for use cases similar to this, where the user wants to supply an arbitrary object to/from the request, without necessarily converting it to a json string. Basically, ComplexScalarGraphType is a more complete version of AnyScalarGraphType except with Name = "Complex" instead of Name = "_Any".

@MakaBuka I noticed your original code parsed a literal object into a string and parsed a variable object into a JObject. I updated my suggested code snippet to unify this behavior and always return a JObject.

@gao-artur Just FYI, in your code snippet, the ParseLiteral implementation only accepts and parses strings (interpreted as json encoded objects) while your ParseValue implementation only accepts and parses objects (which have already been deserialized from their original json). I assume this was the desired behavior, although it is a bit different than @MakaBuka 's original sample and ComplexScalarGraphType which interprets object literals in their native form. For example, this is the format that ComplexScalarGraphType accepts:

{
    // literal as object
    "query": "query ($arg: ComplexScalarGraphType) { testField(arg1: { name: \"john doe\" }, arg2: $arg) }",
    "variables": {
        "arg": { // variable as object
            "name": "john doe"
        }
    }
}

Whereas your scalar would accept this:

{
    // literal as string
    "query": "query ($arg: ComplexScalarGraphType) { testField(arg1: \"{ \\\"name\\\": \\\"john doe\\\" }\", arg2: $arg) }",
    "variables": {
        "arg": { // variable as object
            "name": "john doe"
        }
    }
}

A third variation might accept this:

{
    // literal as string
    "query": "query ($arg: ComplexScalarGraphType) { testField(arg1: \"{ \\\"name\\\": \\\"john doe\\\" }\", arg2: $arg) }",
    "variables": { // variable as string
        "arg": "{ \"name\": \"john doe\" }"
    }
}

Yes, you are right. If I remember correctly this is because of how our FE and automation code send JSON. I forgot this code is not generic enough to share it with people 😀

Hello guys! Thank you for the prompt reply!

@Shane32 @gao-artur I've tried to use your code snippet but unfortunately got an error

System.InvalidOperationException
HResult=0x80131509
Message=Unable to convert 'GraphQLParser.AST.GraphQLObjectValueWithLocation' literal from AST representation to the scalar type 'JSON'
Source=GraphQL
StackTrace:
at GraphQL.Types.ScalarGraphType.ThrowLiteralConversionError(GraphQLValue input, String description)
at GraphQL.Types.ScalarGraphType.ParseLiteral(GraphQLValue value)
at NReco.Graphql.Types.JsonGraphType.ParseLiteral(GraphQLValue value) in
ypes\JsonScalarGraphType.cs:line 16
at GraphQL.Types.ScalarGraphType.CanParseLiteral(GraphQLValue value)

The feature as I got in my project is a complex filter based on json-style format. For example, another query:
query { contacts(filter:{OR: [{id:6},{AND:[{score_geq:215},{country:""Ukraine""}]} ]}) {id name} }

It's more like a condition, not json-object. And that's the issue to get it as it is, as a full non-transformed argument which I parse and apply on the resolver step later.

Shane's code worked for me with your example. But I used NewtonsoftJson instead of STJ. The only thing I fixed is quotes around ""Ukraine""

Well, what's worked for me is inherit from AnyScalarGraphType but the code snippet is the same as @Shane32 provided above

internal class JsonGraphType : AnyScalarGraphType
{
public JsonGraphType() => this.Name = "JSON";

public override object? ParseLiteral(GraphQLValue value)
{
	var jsonString = JsonSerializer.Serialize(base.ParseLiteral(value));
    return jsonString.FromJson<object>();
}

and all tests are green! thanks, guys for the assist and for supporting the project as well!