Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional control over DateTime serialization #283

Closed
ajtribick opened this issue Jul 8, 2024 · 11 comments · Fixed by #298
Closed

Additional control over DateTime serialization #283

ajtribick opened this issue Jul 8, 2024 · 11 comments · Fixed by #298
Assignees
Labels
area:serialization Focused on functional modules of the product enhancement New feature or request

Comments

@ajtribick
Copy link

In order to work around limitations of third-party deserializers, it would be useful to support the following options for DateTime serialization:

  • Ability to serialize the UTC time zone as "Z" rather than "+00:00"
  • Ability to control the number of decimal places in the fractional seconds value

At the moment this is possible by providing a re-implementation of JsonSerializationWriter, which seems excessive for controlling the output of one specific datatype.

@andrueastman
Copy link
Member

Transferring issue as part of #238

@andrueastman andrueastman transferred this issue from microsoft/kiota-serialization-json-dotnet Jul 9, 2024
@andrueastman
Copy link
Member

Thanks for raising this @ajtribick

I believe its possible to override the default behaviour without having to reimplement the JsonSerializationWriter by passing a converter option to the serialization context.

Any chance you've checked out the test here to see how Guid serialization can be changed?

public void WriteGuidUsingConverter()

https://github.com/microsoft/kiota-dotnet/blob/main/tests/serialization/json/Converters/JsonGuidConverter.cs

@andrueastman andrueastman added enhancement New feature or request status:waiting-for-author-feedback Issue that we've responded but needs author feedback to close labels Jul 9, 2024
@andrueastman andrueastman moved this from Needs Triage 🔍 to Waits for author 🔁 in Kiota Jul 9, 2024
@andrueastman andrueastman added the area:serialization Focused on functional modules of the product label Jul 9, 2024
@ajtribick
Copy link
Author

Ah, thanks for that. Serialization side is now resolved. On the deserialization side, I've run into further problems: I need to deserialize in a way that a missing time zone indicator is assumed to be UTC rather than local (i.e. pass DateTimeStyles.AssumeUniversal). Unfortunately, the DateTimeOffset deserialization tries several alternatives before trying the version that takes the KiotaJsonSerializationContext into account, so line 150 successfully deserializes it with the default option that assumes local timezone before the JsonConverter gets involved.

public DateTimeOffset? GetDateTimeOffsetValue()
{
if(_jsonNode.ValueKind != JsonValueKind.String)
return null;
if(_jsonNode.TryGetDateTimeOffset(out var dateTimeOffset))
return dateTimeOffset;
var dateTimeOffsetStr = _jsonNode.GetString();
if(string.IsNullOrEmpty(dateTimeOffsetStr))
return null;
if(DateTimeOffset.TryParse(dateTimeOffsetStr, out dateTimeOffset))
return dateTimeOffset;
return _jsonNode.Deserialize(_jsonSerializerContext.DateTimeOffset);
}

@microsoft-github-policy-service microsoft-github-policy-service bot added Needs: Attention 👋 and removed status:waiting-for-author-feedback Issue that we've responded but needs author feedback to close labels Jul 9, 2024
@andrueastman
Copy link
Member

andrueastman commented Jul 15, 2024

Thanks for the looking into that @ajtribick

I believe what we can do here is probably refactor this to have the first check to be something like this.

     if(_jsonNode.TryGetDateTimeOffset(out var _)) 
         return _jsonNode.Deserialize(_jsonSerializerContext.DateTimeOffset);; 

So that if the node is actually DateTimeOffset(TryGetDateTimeOffset returns true) we should return the deserialized version using the context. Thoughts?
Would you be willing to submit a PR to fix that?

@andrueastman andrueastman added status:waiting-for-author-feedback Issue that we've responded but needs author feedback to close and removed Needs: Attention 👋 labels Jul 15, 2024
@ajtribick
Copy link
Author

Thanks for the suggestion. Is there any reason not to just do something like:

public DateTimeOffset? GetDateTimeOffsetValue()
{
    if(_jsonNode.ValueKind != JsonValueKind.String)
        return null;

    if (string.IsNullOrEmpty(_jsonNode.GetString())
        return null;

    return _jsonNode.Deserialize(_jsonSerializerContext.DateTimeOffset);
}

This would also fix #280 but maybe the additional formats supported by DateTimeOffset.TryParse are necessary here?

@microsoft-github-policy-service microsoft-github-policy-service bot added Needs: Attention 👋 and removed status:waiting-for-author-feedback Issue that we've responded but needs author feedback to close labels Jul 15, 2024
@baywet
Copy link
Member

baywet commented Jul 15, 2024

The challenge with calling directly Deserialize is that it'll throw a Not Supported Exception when it's not able to deserialize something
I believe that Andrew's suggestion to use DateTimeOffset.TryParse first was to safeguard against invalid values. The challenges with that approach is that it'll accept localized formats (not practical in a serialization context) and that it ignores the converter that might be passed.
I'm a little surprised that json element TryParseDateTimeOffset doesn't accept a type info & converter as optional argument. It looks like a shortcoming of the API IMHO.
I believe the reason it does not retain "use the context of the document" might be performance related, they decided to only implement ISO 8601-1
The following stack would confirm that assumption

  1. JsonElement
  2. JsonDocument
  3. Helper

Assuming the team behind STJ is not adding an override any time soon to use the converter for this method, and even if they did, they would probably do so only for net9, which means we couldn't use it with our lower compatibility targets. We should design our own try extension method:

internal static bool TryGetUsingTypeInfo<T>(this JsonElement currentElement, JsonTypeInfo<T> typeInfo, [NotNullWhen(true)] out T deserializedValue) 
{
   try
   {
      deserializedValue = _jsonNode.Deserialize(typeInfo);
      return true;
   } catch (NotSupportedException)
   {
       return false;
   }
}

Which means the method could be simplified to

public DateTimeOffset? GetDateTimeOffsetValue()
{
    if(_jsonNode.ValueKind != JsonValueKind.String)
        return null;

-     if(_jsonNode.TryGetDateTimeOffset(out var dateTimeOffset))
+    if(_jsonNode.TryGetUsingTypeInfo(_jsonSerializerContext.DateTimeOffset, out var dateTimeOffset))
        return dateTimeOffset;
+       else return null;

-    var dateTimeOffsetStr = _jsonNode.GetString();
-    if(string.IsNullOrEmpty(dateTimeOffsetStr))
-        return null;

-    if(DateTimeOffset.TryParse(dateTimeOffsetStr, out dateTimeOffset))
-        return dateTimeOffset;

-    return _jsonNode.Deserialize(_jsonSerializerContext.DateTimeOffset);
}

Thoughts?

@andrueastman
Copy link
Member

To provide a "best effort" attempt at the datetime serialization, I think we can still keep the following before returning null. As the issue here could be priority of parsing as the TryGetUsingTypeInfo may fail and can use this as a fallback rather than losing the information.

    if(DateTimeOffset.TryParse(dateTimeOffsetStr, out dateTimeOffset))
        return dateTimeOffset;

@baywet
Copy link
Member

baywet commented Jul 16, 2024

so effectively this ?

public DateTimeOffset? GetDateTimeOffsetValue()
{
    if(_jsonNode.ValueKind != JsonValueKind.String)
        return null;

-     if(_jsonNode.TryGetDateTimeOffset(out var dateTimeOffset))
+    if(_jsonNode.TryGetUsingTypeInfo(_jsonSerializerContext.DateTimeOffset, out var dateTimeOffset))
        return dateTimeOffset;
+    else if(DateTimeOffset.TryParse(_jsonNode.GetString(), CurrentCulture.InvariantCulture, out dto))
+      return dto;
+    else return null;

-    var dateTimeOffsetStr = _jsonNode.GetString();
-    if(string.IsNullOrEmpty(dateTimeOffsetStr))
-        return null;

-    if(DateTimeOffset.TryParse(dateTimeOffsetStr, out dateTimeOffset))
-        return dateTimeOffset;

-    return _jsonNode.Deserialize(_jsonSerializerContext.DateTimeOffset);
}

@andrueastman
Copy link
Member

Yes. Exactly.

@baywet
Copy link
Member

baywet commented Jul 16, 2024

Alright, now that we have an agreement, @ajtribick @MartinM85 does one of you want to take this on and submit a pull request?

@MartinM85
Copy link
Contributor

@baywet I can modify it as part of #280

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:serialization Focused on functional modules of the product enhancement New feature or request
Projects
Archived in project
Development

Successfully merging a pull request may close this issue.

4 participants