Skip to content

Conversation

@StephenMolloy
Copy link
Member

Fixes #56711

Summary

This PR adds first-class serialization support for DateOnly and TimeOnly across two major .NET XML serialization stacks: DataContractSerializer (DCS/DCJS) and XmlSerializer. The implementation treats these types as primitives (similar to DateTime, TimeSpan, etc.) rather than complex types, ensuring clean schema export/import and efficient round-trip serialization.

Key Changes

Core Implementation

  • Extended primitive registration tables in both DCS and XmlSerializer to recognize DateOnly and TimeOnly as first-class primitives
  • Added canonical wire formats:
    • DateOnlyyyyy-MM-dd (ISO 8601 date format)
    • TimeOnlyHH:mm:ss[.FFFFFFF] (ISO-like time format with optional fractional seconds)
  • Consistent behavior across all three serializers with the same textual representations

Schema Export/Import

  • Custom XSD simple types in each serializer's extension namespace:
    • XmlSerializer: http://microsoft.com/wsdl/types/ (URT namespace)
    • DataContractSerializer: http://schemas.microsoft.com/2003/10/Serialization/
  • Pattern facets restrict lexical forms to prevent offset-bearing inputs that would cause data loss (they function as an executable spec: invalid forms fail at schema validation, not late in parsing)
  • Full round-trip support through XSD export/import tooling (XsdDataContractExporter, XmlSchemaExporter, etc.)

Interoperability Features

  • Attribute override for TimeOnly: [XmlElement(DataType="time")] forces export as standard xs:time for legacy schema compatibility
  • Preserved existing mappings: xs:timeDateTime behavior unchanged for backward compatibility
  • Cross-version safety: Older clients gracefully ignore unknown primitive types

Design Decisions & Rationale

1. Primitive vs Adapter Pattern

Decision: Implement as first-class primitives rather than adapter wrappers (like DateTimeOffsetAdapter).

Why: DateTimeOffset required an adapter historically due to its composite semantics (instant + offset) and late introduction when primitive tables were frozen. DateOnly/TimeOnly are lexically atomic, map cleanly to existing XSD bases, and the primitive infrastructure is now mature and stable.

2. Custom Restricted Types vs Direct XSD Mapping

Decision: Default to custom timeOnly/dateOnly types (restrictions of xs:time/xs:date) rather than direct mapping.

Why: Data preservation is serialization rule #1. Raw xs:time legally permits timezone offsets (13:45:00Z, 09:30:00-05:00) that TimeOnly cannot represent. Using custom restricted types with pattern facets prevents offset-bearing inputs that would force either silent data truncation or late runtime rejection; constraining the lexical space up-front makes the contract explicit and tool-validated.

3. Parsing Strictness

Decision: Enforce canonical forms only—reject offsets, zone indicators, 24:00:00, extra whitespace.

Why: Guarantees deterministic round-trips, prevents implicit data loss, enables simpler/faster parsing, and leaves room for future opt-in relaxed modes without breaking existing contracts.

4. Fractional Seconds Handling

Decision: Emit minimal form (trim trailing zeros, no forced width).

Why: Aligns with existing DateTime serialization behavior across XmlSerializer, DCS, and System.Text.Json (all omit unnecessary trailing fractional zeros). Shows only meaningful precision, produces stable canonical representations, and avoids implying precision that was never present.

5. No Binary Protocol Tokens

Decision: Use textual serialization only, no new binary XML tokens.

Why: Payloads are already minimal (10-15 characters), benefits would be negligible, and adding binary forms would require expanding public writer/reader APIs with significant maintenance overhead for little gain.

Testing Strategy

  • Comprehensive round-trip tests for all three serializers (XML and JSON)
  • Structured schema validation using XSD object model inspection rather than string matching
  • Export/import verification through schema tooling
  • Attribute override testing for TimeOnlyxs:time mapping
  • Backward compatibility validation ensuring existing DateTime behaviors unchanged

Wire Format Examples

XML (DCS & XmlSerializer)

<!-- DateOnly -->
<dateOnly>2024-01-15</dateOnly>

<!-- TimeOnly (default) -->
<timeOnly>14:30:25.123</timeOnly>

<!-- TimeOnly with attribute override -->
<time xsi:type="time">14:30:25.123</time>

JSON (DCJS)

{
  "dateOnlyField": "2024-01-15",
  "timeOnlyField": "14:30:25.123"
}

Schema Export

<!-- Custom restricted types, defined in both the XML serializer and DCS custom namespaces (along with guid, timespan, etc) -->
<xs:simpleType name="dateOnly">
  <xs:restriction base="xs:date">
    <xs:pattern value="([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])" />
  </xs:restriction>
</xs:simpleType>

<xs:simpleType name="timeOnly">
  <xs:restriction base="xs:time">
    <xs:pattern value="([01][0-9]|2[0-3]):([0-5][0-9])(:([0-5][0-9])(\.[0-9]{1,7})?)?" />
  </xs:restriction>
</xs:simpleType>

Backward Compatibility

  • Zero breaking changes: All existing DateTime/DateTimeOffset serialization behaviors preserved
  • Additive only: New primitive support is opt-in by using the new types
  • Cross-version tolerant: Older runtimes that encounter these restricted custom types (derived from xs:date / xs:time) can still deserialize their lexical values into DateTime without information loss, effectively falling back on the base XSD types while ignoring the additional restriction pattern.
  • Schema stability: Existing WSDL/XSD generation unchanged unless new types are used

Future Extensibility

The implementation leaves room for future enhancements:

  • Optional relaxed parsing modes (via AppContext switches)
  • Performance optimizations (binary tokens if justified by telemetry)
  • Tooling improvements (analyzers for interop guidance)
  • Historical JSON context: the legacy /Date(ticks)/ form (still seen with older DateTime serializers) originated before broad ISO 8601 convergence; choosing modern ISO text for these new primitives avoids perpetuating an anachronistic shape.

Files Changed

  • Core serialization: Primitive registration tables and writer/reader delegates
  • Tests: Comprehensive coverage across System.Private.Xml.Tests and System.Runtime.Serialization.Xml.Tests
  • Schema tooling: Export/import logic for XSD generation

This implementation provides a solid foundation for modern date/time serialization while maintaining full backward compatibility and following established .NET serialization patterns.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds first-class serialization support for DateOnly and TimeOnly types across .NET's three major XML serialization stacks: XmlSerializer, DataContractSerializer (DCS), and DataContractJsonSerializer (DCJS). The implementation treats these types as primitives with standardized ISO-like wire formats and includes comprehensive schema export/import support.

Key changes include:

  • Extended primitive registration tables in all serializers to recognize DateOnly and TimeOnly as first-class primitives
  • Added canonical wire formats: DateOnlyyyyy-MM-dd, TimeOnlyHH:mm:ss[.FFFFFFF]
  • Implemented custom XSD schema types with pattern restrictions to prevent data loss from offset-bearing inputs

Reviewed Changes

Copilot reviewed 29 out of 30 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
System.Xml.XmlSerializer/ref/System.Xml.XmlSerializer.cs Added protected method signatures for DateOnly/TimeOnly serialization helpers
XsdDataContractExporterTests/SchemaUtils.cs Added optional parameter to control serialization namespace inclusion in schema dumps
XsdDataContractExporterTests/ExporterTypesTests.cs Updated global type counts and added comprehensive schema export validation tests
XsdDataContractExporterTests/ExporterApiTests.cs Updated expected type counts to account for new primitive types
SerializationTypes.cs Added test wrapper classes for DateOnly/TimeOnly with various attributes and configurations
DataContractSerializer.cs Added comprehensive round-trip tests for DateOnly/TimeOnly in DCS
DataContractJsonSerializer.cs Added JSON serialization tests for DateOnly/TimeOnly primitives
XmlSerializerTests.cs Added extensive XmlSerializer tests including error handling, cross-type compatibility, and schema validation
XmlSerializerTests.RuntimeOnly.cs Added runtime-specific schema import/export tests
Xmlcustomformatter.cs Implemented core formatting/parsing logic for DateOnly/TimeOnly
XmlSerializer.cs Extended primitive serialization dispatch to handle new types
XmlSerializationWriter.cs Added writer methods and primitive type name resolution
XmlSerializationReader.cs Added reader methods and primitive ID registration
XmlReflectionImporter.cs Enhanced DataType attribute handling to support secondary primitive mappings
Types.cs Extended type system with secondary primitive registration and pattern-based schema generation
ReflectionXmlSerializationWriter.cs Added primitive value writing support for DateOnly/TimeOnly
ReflectionXmlSerializationReader.cs Added empty element handling for new primitive types
PrimitiveXmlSerializers.cs Added dedicated serializer implementations for DateOnly/TimeOnly
CodeGenerator.cs Extended constant loading to support DateOnly/TimeOnly constructor calls
LocalAppContextSwitches.cs Added compatibility switch for TimeOnly offset handling
XmlWriterDelegator.cs Implemented DCS writing support for DateOnly/TimeOnly
XmlReaderDelegator.cs Implemented DCS reading support with strict parsing
PrimitiveDataContract.cs Added DataContract implementations for DateOnly/TimeOnly
Globals.cs Added type references and schema definitions for new primitives
DictionaryGlobals.cs Added dictionary entries for primitive type names
DataContract.cs Extended built-in contract registration for DateOnly/TimeOnly

@mconnew
Copy link
Member

mconnew commented Sep 18, 2025

A few notes about binary format. When DateTime is serialized using the binary XmlDictionaryWriter, it writes is using a 64 bit ulong integer. There are 3 options that could be done when writing using a binary xml format.

Write it using the datetime format

The binary XML datetime format writes the underlying 64bit integer as a variable length integer. As a variable length integer, a 64bit integer actually requires 9 bytes on the wire. Along with that, there's a single data type byte emitted, so a total of 10 bytes.

Write it using the textual xml representation

To represent as a string, you have the string itself (10-15 characters/bytes), plus a string data type byte, plus a string length (1 byte when < 127 characters). This makes the length when encoding as a string 12-17 bytes.

Write it using a new binary representation

DateOnly is represented internally as an unsigned 32bit integer. Having a custom binary representation would be 1 byte for data type, 5 bytes for integer value, so 6 bytes total. For TimeOnly, it's internally represented as a 64bit unsigned long, so the same math as DateTime, which is 10 bytes.

To write it using a binary representation, XmlDictionaryWriter/XmlDictionaryReader would need to have support added for it. It would also need the relevant Microsoft protocol documentation updated to add these to it. If there's interest in doing this, we don't need to rush as we can actually defer this to a later time. The existing implementation for reading DateTime with a binary XmlDictionaryReader checks the data type byte, and if it's a string, reads the string from the Xml document then parses it. If the data type byte says it's a variable length integer, then it reads the integer and uses the constructor overload for DateTime which takes the raw integer value (ticks). Basically, DateTime was originally serialized as text like you are doing here, and a later decision was made to switch to binary. The reader was modified to handle either so it's backwards compatible.

I recommend leaving things as they currently are in this PR, reading/writing as text, but be open to adding native binary support later.

@StephenMolloy StephenMolloy marked this pull request as ready for review September 18, 2025 22:47
@StephenMolloy StephenMolloy added this to the 10.0.0 milestone Sep 19, 2025
Copy link
Member

@mconnew mconnew left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@StephenMolloy
Copy link
Member Author

/backport to release/10.0

@github-actions
Copy link
Contributor

…eed to make the test conditional so it doesn't fail AggressiveTrimming test runs.
@StephenMolloy StephenMolloy merged commit bbfaee3 into dotnet:main Sep 22, 2025
88 checks passed
@StephenMolloy
Copy link
Member Author

/backport to release/10.0-rc2

@github-actions
Copy link
Contributor

@github-actions
Copy link
Contributor

@StephenMolloy backporting to "release/10.0-rc2" failed, the patch most likely resulted in conflicts:

$ git am --3way --empty=keep --ignore-whitespace --keep-non-patch changes.patch

Applying: Add DateOnly and TimeOnly as primities for XmlSerializer - With Tests
Applying: Add option to tag TimeOnly fields with DataType=time to allow handling xsd:time while ignoring offsets
Applying: Add DateOnly and TimeOnly as primities for DCS - With Tests
Applying: Add tests for schema import/export considerations.
Applying: Copilot PR feedback.
Applying: Address some PR feedback.
Applying: Different approach to managing AppContext switches and caching
.git/rebase-apply/patch:163: trailing whitespace.
    
warning: 1 line adds whitespace errors.
Using index info to reconstruct a base tree...
M	src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs
M	src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
Falling back to patching base and 3-way merge...
Auto-merging src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs
Auto-merging src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
CONFLICT (content): Merge conflict in src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs
error: Failed to merge in the changes.
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
Patch failed at 0007 Different approach to managing AppContext switches and caching
Error: The process '/usr/bin/git' failed with exit code 128

Please backport manually!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add DateIOnly/TimeOnly support to XmlSerializer and DataContractSerializer

2 participants