Input argument validation

Hows is everyone handling input argument validation? I’m looking at a start/end date validation. I created a DateTimestamp scalar that will ensure they’re valid dates but what is a good approach to ensuring start date is before end date (for example)?

I’m thinking something like this…

getThingForId(
  id: ID, 
  dateTimestampRange: {
    start: DateTimestamp, # <-- scalar ensures valid date, but needs to be before endDate argument
    end: DateTimestamp, # <-- scalar ensures valid date, but needs to be after startDate argument
  }
)

I saw this previous post, but it looks like it went stale and got archived: Validating multiple input arguments

I’ve used directives to validate input arguments before, but I haven’t needed to validate one argument against another. In your case, I think I would handle the validation directly in the resolver.

input CreateUserInput {
  name: String! @validate(maxLength: 50)
  email: String! @validate(email: true, maxLength: 50)
  age: Int! @validate(min: 18)
}
1 Like

I’d like to make the DateTimestampRange re-usable so every resolver that may have a “date range” associated doesn’t have to duplicate validation logic or reach out to some helper for validation but rather build it in (similar to how scalar or directive validation is somewhat “automated”).

My mind mostly goes to something like the @requires directive, but for introducing validation for fields against one another.

If you need to validate the start and end fields in multiple inputs and want to make your schema more expressive, you can create a directive to handle it, like this:

directive @validateDateRange(start: String = "start", end: String = "end") 
  on INPUT_FIELD_DEFINITION

input SubscriptionPeriodInput @validateDateRange(start: "startAt", end: "finishAt") {
  startAt: DateTime
  finishAt: DateTime
}

In this example, the directive validates the startAt and finishAt fields. If another input type uses different field names for a date range, you can simply pass them as arguments to the directive.

4 Likes

Hm - I like that idea. I’ll give it a try. Thanks!

1 Like

A philosophical question is whether to rely solely on GraphQL layer to perform the validation or expect that your underlying code to be resilient itself. BTW, that decision also affects how and where you test this.

At least until now, I haven’t relied on GraphQL frameworks themselves to shield the underlying code from anything. I have, instead, relied on GraphQL to help communicate the constraints and allow early validation (that I don’t rely on).

In other words, GraphQL or not/otherwise, the rest of my code needed to be “safe”, whatever that means in a specific context. It is the primary line of defense. I “annotate” GraphQL schema with directives to help present more meaningful errors to clients, when that is permitted. In some/special cases the same would enable clients to discover/introspect the validation and perform it themselves, for an even more user-friendly approach.

I have some more preferences… Here are 1.5 of them:

  1. If individual values are related to each other but not to the rest of stuff, I’d have a dedicated type for that, kind of like @michaeldouglasdev did with the ...PeriodInput (I’d drop the context). That makes it reusable (both code and docs, everywhere) and extensible.
  2. I like to do the same with “simple” scalars, too. For example, no, IPv4 address is not just a string with validation slapped onto it, it should be its own type. In this case it also helps with “round-tripping” of output values back to inputs. Additionally, if anyone is generating code from GraphQL schema and using strongly typed languages, it will ensure that someone’s favourite breakfast isn’t reused as that IPv4 address even if it looks similar.
2 Likes

A philosophical question is whether to rely solely on GraphQL layer to perform the validation or expect that your underlying code to be resilient itself. BTW, that decision also affects how and where you test this.

I love this question. I wonder if there is a vision from Apollo on this. Is there a world where we could/should consolidate that to just the GraphQL layer in a federated graph, or is the zero-trust policy going to remain the standard (at least in the router/subgraphs).

1 Like

A philosophical question is whether to rely solely on GraphQL layer to perform the validation or expect that your underlying code to be resilient itself.

I like this point. I read it as not “vs” but “and” - not if it validation like this should be in the schema (directive) or resolver layer… but if it should be in the schema (directive) and resolver layer.

I like the portability of the directive but it would also be beneficial to possibly share logic between the directive validation and resolver level validation.