HC14 Strict RFC 3339 DateTime parsing should be opt-in/out - able?
Product
Hot Chocolate
Is your feature request related to a problem?
In updating from HC 13 -> 14 we encountered a runtime behavior change, with respect to dates, I see that this is documented explicitly in the migration notes and I agree that having a common datetime spec is beneficial and rfc 3339 is a reasonable choice. I also realize that 13->14 is a major semver and could be breaking. All that said, I have clients sending me dates that are not strictly compatible, and while I agree they're not doing the correct thing and can and should be fixed, it's unfortunate this new spec enforcement is coupled directly with upgrading to 14.
My proposal would be to allow the ability to opt-in to the legacy datetime parser similar to how I can do the same for the legacy node ID serializer. It gives me time to migrate clients while the server can go from 13 to 14. For dates though, I have published clients, who from their point of view are sending the same query to the same schema and are now broken just for me having updated the server from 13 to 14. I need some way to decouple the 13 -> 14 upgrade from the strict enforcement of the datetime spec the same way I can decouple the upgrade from the new ID serializer.
I'm left with few options, either the client has to change, our clients go through app approval processes on various platforms, which again I can't always force (some platforms won't force an update even if one is available), especially at an exact time (like when the hc14 server starts taking traffic), or I have to upgrade and deploy elsewhere so new clients that are sending rfc compatible dates can hit an hc 14 instance (without a schema change) and those that haven't updated can still point to the hc13 version (with the same schema) and run dual processes for some period of time.
Specifically the error was:
DateTime cannot parse the given literal of type StringValueNode
For this date (missing seconds)
"2024-08-23T00:00-04:00"
The solution you'd like
allow the ability to opt-in to the legacy datetime parser
I was able to work around this by modifying your private static scalar lookups using just the right amount of unholy reflection and replacing your datetime scalar with a child of your datetime scalar that doesn't call the DateTimeScalarRegex. This is a less-than-ideal solution.
Why dont you use BindRuntimeType?
Is there a way to replace the DateTime scalar type that HotChcoalte register? BindRuntimeType adds a new scalar type. That cannot have the same name. This makes client generation harder down the line.
builder.Services
.AddGraphQLServer()
.BindRuntimeType<DateTime, CustomDateTimeType>()
.AddTypes();
CustomDateTimeType is a copy of DateTimeType, with the format check removed.
Yeah but that would end up with me having two types of scalars for date time?
Thats what i meant with problems generating clients down the line. Since they cant both have the name DateTime. It will throw an exception on startup. Key already exist. Thats why i wanted to remove the old one.
I don't see that.
Same as magahl, I get this error when trying to do the workaround:
An item with the same key has already been added. Key: DateTime
at HotChocolate.Configuration.TypeInitializer.DiscoverTypes()
at HotChocolate.Configuration.TypeInitializer.Initialize()
at HotChocolate.SchemaBuilder.Setup.InitializeTypes(SchemaBuilder builder, IDescriptorContext context, IReadOnlyList`1 types)
at HotChocolate.SchemaBuilder.Setup.Create(SchemaBuilder builder, LazySchema lazySchema, IDescriptorContext context)
at HotChocolate.SchemaBuilder.Create(IDescriptorContext context)
I was able to work around this by modifying your private static scalar lookups using just the right amount of unholy reflection and replacing your datetime scalar with a child of your datetime scalar that doesn't call the DateTimeScalarRegex. This is a less-than-ideal solution.
Hey @randyridge could you tell me how you did this? My approach was to try and modify the static regex in DateTimeType, but newer versions of dotnet won't let me be that naughty (because it's static readonly and the class is initialised before I can get to it)
In our startup something like so:
var lookupFieldValue = typeof(Scalars).GetField("_lookup", BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null);
if(lookupFieldValue == null) {
throw new InvalidOperationException("Could not find _lookup field on Scalars type.");
}
var lookup = (Dictionary<Type, Type>) lookupFieldValue;
lookup.Remove(typeof(DateTime));
lookup.Remove(typeof(DateTimeOffset));
lookup.Add(typeof(DateTime), typeof(LooseyGooseyDateTimeType));
lookup.Add(typeof(DateTimeOffset), typeof(LooseyGooseyDateTimeType));
var nameLookupFieldValue = typeof(Scalars).GetField("_nameLookup", BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null);
if(nameLookupFieldValue == null) {
throw new InvalidOperationException("Could not find _nameLookup field on Scalars type.");
}
var nameLookup = (Dictionary<string, Type>) nameLookupFieldValue;
nameLookup[ScalarNames.DateTime] = typeof(LooseyGooseyDateTimeType);
and then...
using System;
using System.Globalization;
using HotChocolate.Language;
using HotChocolate.Types;
public class LooseyGooseyDateTimeType : DateTimeType {
public LooseyGooseyDateTimeType() : base(ScalarNames.DateTime) {
}
public override bool TryDeserialize(object? resultValue, out object? runtimeValue) {
switch(resultValue) {
case null:
runtimeValue = null;
return true;
case string s when TryDeserializeFromString(s, out var d):
runtimeValue = d;
return true;
case DateTimeOffset dto:
runtimeValue = dto;
return true;
case DateTime dt:
runtimeValue = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
return true;
default:
runtimeValue = null;
return false;
}
}
protected override DateTimeOffset ParseLiteral(StringValueNode valueSyntax) {
if(TryDeserializeFromString(valueSyntax.Value, out var value)) {
return value!.Value;
}
throw new SerializationException("LooseyGooseyDateTimeType: Cannot parse literal.", this);
}
private static bool TryDeserializeFromString(string? serialized, out DateTimeOffset? value) {
// No "Unknown Local Offset Convention" - https://www.graphql-scalars.com/date-time/#no-unknown-local-offset-convention
if(string.IsNullOrEmpty(serialized) || serialized.EndsWith("-00:00")) {
value = null;
return false;
}
if(serialized.EndsWith('Z') && DateTime.TryParse(serialized, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var zuluTime)) {
value = new DateTimeOffset(zuluTime.ToUniversalTime(), TimeSpan.Zero);
return true;
}
if(DateTimeOffset.TryParse(serialized, out var dt)) {
value = dt;
return true;
}
value = null;
return false;
}
}
ymmv
@randyridge thanks for that, I figured all that out but what I also needed to do was replace the references to it in the filtering. Because we have .UseFiltering() there are some filters that reference the original DateTimeType.
Here's how I did that:
...
.AddFiltering<ReplaceDateTimeStrictParsingHackConvention>()
public class ReplaceDateTimeStrictParsingHackConvention : FilterConvention
{
private static int _lookupPatched = 0;
protected override void Configure(IFilterConventionDescriptor descriptor)
{
// Use reflection to patch in our relaxed DateTime scalar type, gross I know.
// Only do this once, lock-free so the tests are a bit faster.
if (Interlocked.CompareExchange(ref _lookupPatched, 1, 0) == 0)
{
var type = typeof(HotChocolate.Types.Scalars);
var lookupField = type.GetField("_lookup", BindingFlags.NonPublic | BindingFlags.Static);
var lookupVal = lookupField?.GetValue(null) as Dictionary<Type, Type>;
lookupVal!.Remove(typeof(DateTime));
lookupVal!.Remove(typeof(DateTimeOffset));
lookupVal![typeof(DateTime)] = typeof(RelaxedParsingDateTimeType);
lookupVal![typeof(DateTimeOffset)] = typeof(RelaxedParsingDateTimeType);
var nameField = type.GetField("_nameLookup", BindingFlags.NonPublic | BindingFlags.Static);
var nameVal = nameField?.GetValue(null) as Dictionary<string, Type>;
nameVal!.Remove(ScalarNames.DateTime);
nameVal![ScalarNames.DateTime] = typeof(RelaxedParsingDateTimeType);
}
// Replace other references to the strict datetime type.
descriptor.AddDefaults();
descriptor.Operation(-420).Name("doesNotAppearInSchema");
descriptor
.BindRuntimeType<DateTime, RelaxedParsingDateTimeOperationFilterInputType>()
.BindRuntimeType<DateTime?, RelaxedParsingDateTimeOperationFilterInputType>()
.BindRuntimeType<DateTimeOffset, RelaxedParsingDateTimeOperationFilterInputType>()
.BindRuntimeType<DateTimeOffset?, RelaxedParsingDateTimeOperationFilterInputType>();
}
}
So we tested this whole thing and its fairly easy to swap the scalar out including filtering. Its about 3 - 4 lines of code. We have put it on our YouTube list to create a walkthrough.
Hi @michaelstaib Can you please put those 3-4 lines of code here in the issue :)
So we tested this whole thing and its fairly easy to swap the scalar out including filtering. Its about 3 - 4 lines of code. We have put it on our YouTube list to create a walkthrough.
YouTube != documentation. Please don't do this to us. You clearly have the fix already. Holding us hostage on a todo-list isn't helpful.
Apologies for the delayed response.
The following should work:
builder.Services
.AddGraphQLServer()
.AddType<CustomDateTimeType>()
.BindRuntimeType<DateTime, CustomDateTimeType>() // If you're also using the C# DateTime type.
.BindRuntimeType<DateTimeOffset, CustomDateTimeType>()
.AddFiltering(
c => c
.AddDefaults()
.BindRuntimeType<DateTime, CustomDateTimeFilterInputType>() // If you're also using the C# DateTime type.
.BindRuntimeType<DateTimeOffset, CustomDateTimeFilterInputType>())
.AddTypes();
public sealed class CustomDateTimeFilterInputType : ComparableOperationFilterInputType<DateTimeOffset>;
@glen-84 The solution does not work and produces
Any idea how to work around this? The datetime changes are causing us some headaches
@RyanTotalEto are you using schema-first? or name bindings?
Hi @michaelstaib and @glen-84 , We have just upgraded from 13 to 15 and now we have regression issues on date filters. Our clients are using queries with filters like below. I have tried the above solution by adding the filter conversion, but we still get the error:
{
"errors": [
{
"message": "DateTime cannot parse the given literal of type `StringValueNode`.",
"locations": [
{
"line": 2,
"column": 13
}
],
"path": [
"gte"
],
"extensions": {
"fieldCoordinate": "DateTimeOperationFilterInput.gte",
"fieldType": "DateTime"
}
}
],
"data": {
"outputChannelLogs": null
}
}
sendDate: {
gte: "2025-02-01",
lte: "2025-02-28",
}
descriptor
.BindRuntimeType<DateTime, CustomDateTimeFilterInputType>() // If you're also using the C# DateTime type.
.BindRuntimeType<DateTimeOffset, CustomDateTimeFilterInputType>();
I see this issue is closed. I will ask on Slack as well
Here's a working example: https://github.com/glen-84/DateTimeWorkaround/.
@glen-84 Unfortunately that is not a working example, it fails to build with
DateTimeWorkaround/Program.cs(11,6): error CS0121: The call is ambiguous between the following methods or properties: 'SchemaRequestExecutorBuilderExtensions.AddTypes(IRequestExecutorBuilder, params Type[])' and 'SchemaRequestExecutorBuilderExtensions.AddTypes(IRequestExecutorBuilder, params INamedType[])'
It is a working example. The error that you're seeing is related to the source generator.
You can try the following:
- Make sure that your IDE and SDK are up-to-date.
- Delete all bin/obj directories.
- Restart your IDE, clearing caches if necessary.
Finally! I got it working after spending almost a day.
Thanks to @glen-84 for providing working example, but there are few more under the hood things to do make it working in other projects.
I used to get same error as @mnordland-newclassrooms getting :
DateTimeWorkaround/Program.cs(11,6): error CS0121: The call is ambiguous between the following methods or properties: 'SchemaRequestExecutorBuilderExtensions.AddTypes(IRequestExecutorBuilder, params Type[])' and 'SchemaRequestExecutorBuilderExtensions.AddTypes(IRequestExecutorBuilder, params INamedType[])'
Here are important points to follow to overcome this:
- Add following package to project.
<PackageReference Include="HotChocolate.Types.Analyzers" Version="15.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Important: add Properties/ModuleInfo.cs with the line [assembly: HotChocolate.Module("Types")]
This generates AddTypes() extension method during build and the error will go away.
Got clue from Google and here is another reference HotChocolate 13 type auto registration
- In case if you are having your own custom filter extensions (Refer my gist at GraphQL Hot Chocolate Custom filter operation example. Implements operations "like" , "ci_like" and "ci_equals". For case sensitive and insensitive ("ci_...")
You need to use "Using overriding the FilterConventionExtension Conventions" otherwise you will get error of type already added.
Add custom filter after AddFiltering(...)
- Also, if your model uses nullable DateTime,
DateTime?add that as type again along with DateTime.
Here is the graphql definition from my working code:
builder.Services
.AddGraphQLServer()
.ModifyRequestOptions(o => o.IncludeExceptionDetails = builder.Environment.IsDevelopment())
.ModifyCostOptions(o => o.EnforceCostLimits = false)
.RegisterDbContextFactory<DataApiContext>() // take it for parallel execution
.InitializeOnStartup()
.AddType<CustomDateTimeType>()
.BindRuntimeType<DateTime, CustomDateTimeType>()
.BindRuntimeType<DateTime?, CustomDateTimeType>()
.BindRuntimeType<DateTimeOffset, CustomDateTimeType>()
.BindRuntimeType<DateTimeOffset?, CustomDateTimeType>()
.AddTypes()
.AddPagingArguments()
.AddProjections()
.AddFiltering(
c => c
.AddDefaults()
.BindRuntimeType<DateTime, CustomDateTimeFilterInputType>()
.BindRuntimeType<DateTime?, CustomDateTimeFilterInputType>()
.BindRuntimeType<DateTimeOffset, CustomDateTimeFilterInputType>()
.BindRuntimeType<DateTimeOffset?, CustomDateTimeFilterInputType>()
)
.AddConvention<IFilterConvention, CustomFilterConventionExtension>() /* Using overriding the FilterConventionExtension Conventions. It has to be placed after AddFiltering()*/
.AddSorting()
.AddQueryType<Query>() // or use [QueryType]
.AddMutationType<Mutation>();
For anyone reading this, an option was added in 15.1.4 that allows you to disable the format check:
service.AddGraphQLServer().AddType(new DateTimeType(disableFormatCheck: true))
However, I would strongly recommend updating your clients over time, since the option may be removed in a future release. The type is supposed to represent a valid RFC 3339 date-time, and should not be used for dates only (or other incomplete date-times).
Thanks @glen-84, But It sincerely hope that HotChocolate minimises breaking changes and feature removals in every new version. That’s problematic many times when we are upgrading libraries for projects which are already on production.
Regards Tejasvi
From: Glen @.> Sent: Saturday, June 14, 2025 7:40:49 PM To: ChilliCream/graphql-platform @.> Cc: Tejasvi Hegde @.>; Comment @.> Subject: Re: [ChilliCream/graphql-platform] HC14 Strict RFC 3339 DateTime parsing should be opt-in/out - able? (Issue #7402)
[https://avatars.githubusercontent.com/u/261509?s=20&v=4]glen-84 left a comment (ChilliCream/graphql-platform#7402)https://github.com/ChilliCream/graphql-platform/issues/7402#issuecomment-2972784560
For anyone reading this, an option was added in 15.1.4 that allows you to disable the format check:
service.AddGraphQLServer().AddType(new DateTimeType(disableFormatCheck: true))
However, I would strongly recommend updating your clients over time, since the option may be removed in a future release. The type is supposed to represent a valid RFC 3339 date-time, and should not be used for dates only (or other incomplete date-times).
— Reply to this email directly, view it on GitHubhttps://github.com/ChilliCream/graphql-platform/issues/7402#issuecomment-2972784560, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ACEDVH334MFMAXR2H32NSPL3DQUOTAVCNFSM6AAAAABNAVSNUKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDSNZSG44DINJWGA. You are receiving this because you commented.Message ID: @.***>
Breaking changes can and should be expected in major versions. This was essentially a bug fix, but it was still released in a major version and noted in the migration guide.
This worked! Thank you. We can now look at moving from 13 :-)
I am already using 15. It’s working 😊
Regards Tejasvi
From: Jaco De Villiers @.> Sent: Wednesday, June 18, 2025 6:25:42 PM To: ChilliCream/graphql-platform @.> Cc: Tejasvi Hegde @.>; Comment @.> Subject: Re: [ChilliCream/graphql-platform] HC14 Strict RFC 3339 DateTime parsing should be opt-in/out - able? (Issue #7402)
[https://avatars.githubusercontent.com/u/5814127?s=20&v=4]jacodv left a comment (ChilliCream/graphql-platform#7402)https://github.com/ChilliCream/graphql-platform/issues/7402#issuecomment-2984086846
This worked! Thank you. We can now look at moving from 13 :-)
— Reply to this email directly, view it on GitHubhttps://github.com/ChilliCream/graphql-platform/issues/7402#issuecomment-2984086846, or unsubscribehttps://github.com/notifications/unsubscribe-auth/ACEDVH5GMHBZSQC725VOUJL3EFOU5AVCNFSM6AAAAABNAVSNUKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDSOBUGA4DMOBUGY. You are receiving this because you commented.Message ID: @.***>