Stylianos Gakis
11/26/2024, 9:32 AMStylianos Gakis
11/26/2024, 9:33 AMmbonnin
11/26/2024, 9:45 AMmbonnin
11/26/2024, 9:46 AM@semanticNonNull
you can read more about it there: https://www.apollographql.com/docs/kotlin/advanced/nullabilitymbonnin
11/26/2024, 9:47 AMmbonnin
11/26/2024, 9:49 AMThe thing is, there is a client-side directive to potentially turn nullable fields to nonnull, but there isn't the oppositeThere is
@semanticNonNull
to turn a field that is null only for errors into a non-null Kotlin properties. Note that ideally it's applied server-side so that it can be shared cross teams (the backend team knows whether a field is null only for errors)
There is kind of the opposite with @catch
which allows you to decide field by field what to do with errors: conflate them to null
again, throw the whole query or expose a FieldResult
classStylianos Gakis
11/26/2024, 1:16 PMtype User {
id: ID!
# name is never null unless there is an error
name: String @semanticNonNull
# avatarUrl may be null even if there is no error. In that case the UI should be prepared to display a placeholder.
avatarUrl: String
}
What apollo-kotlin will then generate is
data class User(
val id: String,
val name: String,
val avatarUrl: String?,
)
Since we know that name
should be null in all normal scenarios.
So far so good, on the client nothing has changed as far as the generated code goes. But there is semantic meaning now in the schema to what null may mean for that field.
--- Here comes the part I am unsure about
Then to make use of this new information on the client, one would need to mark it with @catch
on the operation level now, not on the schema, on a case-by-case basis.
And there we can decide on the three options as described in the docs:
β’ handle errors as FieldResult<T>
, getting access to the colocated error.
β’ throw the error and let another parent field handle it or bubble up to data == null
.
β’ coerce the error to null
(current GraphQL default).
If we do nothing (aka do not add the @catch
, we will fall-back to just the third option, which is to turn it to null, and since the code-gen has generated that as non-null, the error will bubble up the exact way it was doing before too.
And for scenarios where nothing is nullable, it will, just like before, bubble all the way to the root of the response, making all of it null and the errors response will be populated with the same errors there.
OR
Is it that by default now it will mark those fields as nullable in Kotlin, which would be a change in behavior? As I understand the three options now, I thought that the second option, aka @catch(to: THROW)
would be the behavior that preserves the previous behavior.Stylianos Gakis
11/26/2024, 1:18 PMFoo!
already, and I would like them to perhaps change to in the future be @semanticNotNull Foo
.
Would this be a migration which can be done in a way which will not break older clients? As I understand, if the schema does a Foo!
-> Foo
change, that is a breaking change in a sense, but if it does a Foo!
-> @semanticNotNull Foo
change this will "change" the schema itself, but practically they would mean the same thing right?
And if we combine that with having a default of @catch(to: THROW)
that would preserve the same functionality as before doing this change too.
Does what I say make sense here?Stylianos Gakis
11/26/2024, 1:43 PMString!
to @semanticNotNull String
β’ Existing clients who were built with the old schema before this change:
This will mean that now the schema we built with was expecting that to be not-null, so when it is null, the parsing will fail, and I hope this will the surface in a way where the null will bubble up, and the errors will be populated. So the old clients will interpret that as a complete failure, and the "bad path" will be taken, just it does today when backend gets some internal exception and returns null anyway.
β’ On new clients who know about @semanticNotNull:
We will now be able to see the @semanticNotNull, the type will still be generated as non-null, and if we do nothing and we do not annotate with @catch
, the error will bubble up the same way upwards, and we will get the same behavior as I describe above
β’ On new clients who know about this and have opted into using @catch
in specific operations:
We will then be able to be more smart about this, even if that means using the FieldResult
or a normal Kotlin nullable type. If we don't opt-in, nothing should change from the old behavior.
Does this sound correct, or am I missing something? πmbonnin
11/26/2024, 1:48 PMgo fromThat's typically not what you would dotoString!
@semanticNotNull String
String!
is "stronger" than @semanticNotNull String
mbonnin
11/26/2024, 1:48 PMString
to @semanticNotNull String
Stylianos Gakis
11/26/2024, 2:05 PM!
and our only option to be able to break this query down in a way where partial data would work is to split it in 10 separate queries, so that they can fail individually and we can stitch back the data class from all those separate queries.
So the problem is that we've eagerly set a lot of things as !
now already in the schema, and that limits our ability to ever have partial responses. And if 1 thing goes bad, entire screens completely fail because they could not render a nice pretty thingy on the side that is completely optional for that screen.
Does this use case make sense?mbonnin
11/26/2024, 2:08 PMmbonnin
11/26/2024, 2:09 PMStylianos Gakis
11/26/2024, 2:10 PMmbonnin
11/26/2024, 2:11 PMmbonnin
11/26/2024, 2:11 PMExisting clients who were built with the old schema before this changeThose will see
response.exception != null
and should fail the whole queryStylianos Gakis
11/26/2024, 2:11 PMresponse.exception != null
and should fail the whole query
That would be perfect then! As this is what those clients experience today anyway with our current setup!mbonnin
11/26/2024, 2:12 PMmbonnin
11/26/2024, 2:12 PM@catch
in specific operations:
Note that you have to opt-in a default @catchByDefault(to: CatchTo)
mbonnin
11/26/2024, 2:13 PM@catchByDefault(to: NULL)
mbonnin
11/26/2024, 2:13 PMStylianos Gakis
11/26/2024, 2:13 PM@catchByDefault(to: CATCH)That'd be
extend schema @catchByDefault(to: THROW)
you mean right?mbonnin
11/26/2024, 2:14 PMNULL
, THROW
, RESULT
at the schema level when opting-in error aware parsingmbonnin
11/26/2024, 2:14 PMextend schema @catchByDefault(to: NULL)
but I think I like extend schema @catchByDefault(to: THROW)
bettermbonnin
11/26/2024, 2:14 PMStylianos Gakis
11/26/2024, 2:15 PMnull
things like that, throwing feels like a sane default preserving the old behavior for us. And we can then take it on a case by case basis if some do not want this behavior.mbonnin
11/26/2024, 2:16 PMStylianos Gakis
11/26/2024, 2:16 PMmbonnin
11/26/2024, 2:17 PMString! => @semanticNonNull
, extend schema @catchByDefault(to: THROW)
is the "compatible" option I guessmbonnin
11/26/2024, 2:17 PMmbonnin
11/26/2024, 2:20 PMString! => @semanticNonNull String
(which is cool!), your clients will bump into https://github.com/graphql/graphql-spec/issues/300 as @semanticNonNull
is lost in introspection π so you'll need to find a way to ditribute your schema that is not relying on introspectionStylianos Gakis
11/26/2024, 2:20 PMmbonnin
11/26/2024, 2:20 PMmbonnin
11/26/2024, 2:22 PMStylianos Gakis
11/26/2024, 2:22 PMmbonnin
11/26/2024, 2:23 PMapollo-client
nor apollo-ios
yetStylianos Gakis
11/26/2024, 2:25 PM!
instead of nullable with this directivembonnin
11/26/2024, 2:27 PMmbonnin
11/26/2024, 2:29 PMgraphql-javaAt some point I think GraphQL java added
AppliedDirective
to introspection which would fix #300. I'm not 100% sure though, would need to look that up
a different version of the schema compared to the web and iOS which would still expect all of those to beYup, I guess a runtime switch on the server would be nice. This is amongst the discussed options right nowinstead of nullable with this directive!
Stylianos Gakis
11/26/2024, 2:29 PMmbonnin
11/26/2024, 2:36 PM@noBubblesPlease
:
query GetUser @noBubblesPlease {
user {
name
email
}
}
mbonnin
11/26/2024, 2:36 PMtype User {
name: String!
email: String!
}
mbonnin
11/26/2024, 2:37 PM@noBubblesPlease
-compliant server would reply with
{
"data": "user": { "name": "foo", "email": null },
"errors": [{"message": "oops", location: ["user", "email"]}]
}
Stylianos Gakis
11/26/2024, 2:38 PMemail
throws, it would have to be null, and name would have to be present.
But both would need to be nullable on the code generated to support either of the two cases where either might throw internally right?mbonnin
11/26/2024, 2:38 PM@catch
Stylianos Gakis
11/26/2024, 2:39 PMmbonnin
11/26/2024, 2:40 PMmbonnin
11/26/2024, 2:40 PMStylianos Gakis
11/26/2024, 2:40 PM@catch
mbonnin
11/26/2024, 2:41 PMStylianos Gakis
11/26/2024, 2:41 PMStylianos Gakis
11/26/2024, 2:42 PMmbonnin
11/26/2024, 2:44 PMmbonnin
11/26/2024, 2:44 PMmbonnin
11/26/2024, 2:59 PMmbonnin
11/26/2024, 3:00 PM@defer
, oneOf
, etc... It's quite important for field feedbackStylianos Gakis
11/26/2024, 3:04 PMmbonnin
11/26/2024, 3:20 PMStylianos Gakis
11/26/2024, 3:26 PM@catch
in this context, won't the first question be how should clients handle this if they do not know how to change their generated code to account for this directive in the first place?
Or is the hope that if such an experimental directive exists in the backend, the clients can then more easily build the necessary tooling required to make it work? And just so it happens that apollo-kotlin is in a perfect spot to support it, since it already supports @semanticNotNull
?mbonnin
11/26/2024, 3:29 PMIs your hope that such a directive would be faster to get any sort of experimental support compared to something which may be more involved like @semanticNotNull?Yes π It's much simpler
Since you do not mention anything aroundMaybe? Let's see. The example shows thatin this context, won't the first question be how should clients handle this@catch
user.name
is returned with @noBubbling
. How the client handle this felt irrelevant for that specific backend issue but I'll add more context if needed.
if such an experimental directive exists in the backend, the clients can then more easily build the necessary tooling required to make it work?Exactly. This is what I hope with this. It doesn't add a type, it "just" changes the execution logic a tiny bit
mbonnin
11/26/2024, 3:29 PMStylianos Gakis
11/26/2024, 3:32 PMmbonnin
11/26/2024, 3:32 PMmbonnin
12/09/2024, 5:12 PM@errorHandling(onError: NULL)
(previously known as @noBubbling
) there:
https://github.com/martinbonnin/graphql-java/?tab=readme-ov-file#graphql-java-forkStylianos Gakis
12/09/2024, 8:46 PMmbonnin
12/09/2024, 9:49 PMmbonnin
12/09/2024, 9:50 PMmbonnin
12/09/2024, 9:50 PMStylianos Gakis
12/09/2024, 10:15 PMmbonnin
12/09/2024, 11:12 PM