Skip to content

[Breaking]: Normalize ISerializer argument validation and enhance test coverage#440

Merged
niemyjski merged 6 commits into
mainfrom
feature/normalize-ISerializer-argument-validation
Jan 22, 2026
Merged

[Breaking]: Normalize ISerializer argument validation and enhance test coverage#440
niemyjski merged 6 commits into
mainfrom
feature/normalize-ISerializer-argument-validation

Conversation

@niemyjski

@niemyjski niemyjski commented Jan 21, 2026

Copy link
Copy Markdown
Member

Serializer Test Coverage Enhancement

Summary

  1. Normalize error handling to match System.Text.Json (industry standard)
  2. Add argument validation to SerializerExtensions methods
  3. Rename existing tests to follow naming convention
  4. Add comprehensive test coverage with grouped validation tests
  5. Update all concrete serializer test classes
  6. Run full test suite

Breaking Changes

The following breaking changes were introduced to improve consistency and correctness:

Null Value Serialization

Method Previous Behavior New Behavior
SerializeToBytes(null) Returned null Returns serialized null (e.g., [110, 117, 108, 108] for JSON "null")
SerializeToString(null) Returned null Returns "null" string for JSON serializers
Deserialize<T>((string)null) Returned default value Throws ArgumentException
Deserialize<T>("") Returned default value Throws ArgumentException

Migration: If your code checks for null return values from serialization methods, update it to handle the serialized null representation instead.

SystemTextJsonSerializer Primitive Type Handling

When deserializing to object type, the returned types for primitives have changed:

Value Type Previous Behavior New Behavior
Integers (fits in int32) long int
Integers (fits in int64) long long
Decimal values double decimal (when precise)
Date strings DateTime DateTimeOffset (preserves timezone)

Migration: If your code performs type checks like if (value is long), update to handle the new types or use pattern matching with multiple types.


Part 1: Update SerializerExtensions

File: src/Foundatio/Serializer/ISerializer.cs

  • Added ArgumentNullException.ThrowIfNull() for serializer parameter in all extension methods
  • Added ArgumentNullException.ThrowIfNull() for data parameter (Stream, byte[])
  • Added ArgumentException.ThrowIfNullOrEmpty() for string data parameter
  • Changed ArgumentOutOfRangeException.ThrowIfZero() to ArgumentException for empty byte arrays (consistency with documentation)
  • Changed null checks to use is null pattern matching

Part 2: Rename Existing Tests

Current Name New Name
CanRoundTripBytes Deserialize_WithValidBytes_ReturnsDeserializedObject
CanRoundTripString Deserialize_WithValidString_ReturnsDeserializedObject
CanHandlePrimitiveTypes Deserialize_WithPrimitiveType_ReturnsValue

Part 3: New Tests in SerializerTestsBase

File: src/Foundatio.TestHarness/Serializer/SerializerTestsBase.cs

Added comprehensive virtual test methods:

  • Deserialize_WithInvalidInput_ThrowsArgumentException - grouped validation test
  • Deserialize_WithUnicodeAndSpecialCharacters_PreservesContent - Unicode test
  • Deserialize_WithValidStream_ReturnsDeserializedObject - stream overload test
  • Deserialize_WithNumericPrimitivesToObject_ReturnsCorrectTypes - numeric type coercion test
  • SerializeToBytes_WithNullValue_ReturnsNull - null value test
  • SerializeToString_WithNullValue_ReturnsNull - null value test

All tests follow the Arrange/Act/Assert pattern with proper comments.


Part 4: Updated All Concrete Test Classes

Updated all 6 serializer test classes with alphabetically ordered test overrides:

  • CompressedMessagePackSerializerTests.cs
  • JsonNetSerializerTests.cs
  • MessagePackSerializerTests.cs
  • SystemTextJsonSerializerTests.cs (both classes)
  • Utf8JsonSerializerTests.cs

Each concrete test class now has comprehensive test coverage.


Documentation Updates

File: docs/guide/serialization.md

  • Fixed incorrect statement about null serialization behavior
  • Added comprehensive "Breaking Changes" section with migration guidance
  • Updated validation behavior documentation

Test Results

  • 90 serializer tests pass (15 tests × 6 serializer classes)
  • 1769 total tests pass (full test suite)
  • 11 benchmarks skipped

Code Style

  • Added blank lines after Assert statements
  • Added blank lines after closing braces }
  • Tests test the ISerializer/ITextSerializer interface
  • All serializers behave identically for the same inputs
  • One grouped validation test covers all invalid input scenarios for Deserialize

- Add ArgumentNullException.ThrowIfNull() for serializer parameter in all extension methods
- Add ArgumentNullException.ThrowIfNull() for data parameter (Stream, byte[])
- Add ArgumentException.ThrowIfNullOrEmpty() for string data parameter
- Add ArgumentOutOfRangeException.ThrowIfZero() for empty byte arrays
- Rename existing tests to follow MethodName_State_Expected pattern
- Add 5 new virtual test methods for comprehensive coverage
- Update all 6 concrete serializer test classes with alphabetically ordered tests
- Add Arrange/Act/Assert comments to all tests
- All 48 serializer tests pass, 1727 total tests pass
…on, and test coverage

- Fix resource disposal in SystemTextJsonSerializer (Utf8JsonWriter) and JsonNetSerializer (JsonTextWriter, StreamWriter)
- Normalize parameter names across all ISerializer implementations to match interface
- Add XML documentation to ISerializer and ITextSerializer interfaces
- Add whitespace validation (ThrowIfNullOrWhiteSpace) for string deserialization
- Add comprehensive test coverage: empty collections, null properties, DateTime, numeric types
- Update serialization documentation with extension methods and validation behavior
@niemyjski

Copy link
Copy Markdown
Member Author

Additional Changes (Second Commit)

This commit adds further improvements to the serializer infrastructure:

1. Resource Disposal Fixes

SystemTextJsonSerializer.cs:

  • Added using statement to properly dispose Utf8JsonWriter

JsonNetSerializer.cs:

  • Added using statements for both StreamWriter and JsonTextWriter
  • Added leaveOpen: true to StreamWriter to prevent closing the output stream

2. Parameter Name Normalization

All implementations now match the interface parameter names:

File Changes
SystemTextJsonSerializer.cs datavalue, outputStreamoutput, inputStreamdata
JsonNetSerializer.cs datavalue, outputStreamoutput, inputStreamdata
MessagePackSerializer.cs datavalue, inputdata
Utf8JsonSerializer.cs datavalue, inputdata

3. Interface Documentation

Added comprehensive XML documentation to:

  • ISerializer interface and its methods
  • ITextSerializer marker interface

4. Whitespace Validation

  • Changed ArgumentException.ThrowIfNullOrEmpty to ArgumentException.ThrowIfNullOrWhiteSpace for string deserialization methods
  • Added whitespace test case (" ") to Deserialize_WithInvalidInput_ThrowsArgumentException

5. Additional Test Coverage

New tests added to SerializerTestsBase:

  • Serialize_WithEmptyCollection_ReturnsValidOutput
  • Serialize_WithNullPropertyInObject_HandlesCorrectly
  • Serialize_WithDateTimeValue_PreservesValue
  • Serialize_WithNumericTypes_PreservesValues

6. Documentation Updates

Updated /docs/guide/serialization.md:

  • Added extension methods documentation
  • Documented input validation behavior
  • Clarified ITextSerializer marker interface purpose
  • Added UTF-8 vs Base64 encoding explanation for text vs binary serializers

Test Results

  • Build: ✅ Succeeded with 0 warnings
  • Serializer Tests: ✅ 72 passed, 5 skipped (benchmarks)
  • Full Test Suite: ✅ 1751 passed, 11 skipped

@niemyjski

niemyjski commented Jan 21, 2026

Copy link
Copy Markdown
Member Author

Question: SerializeToBytes/SerializeToString Null Handling

Currently, when value is null, the serialization methods return null rather than throwing an exception:

public static byte[] SerializeToBytes<T>(this ISerializer serializer, T value)
{
    ArgumentNullException.ThrowIfNull(serializer);

    if (value is null)
        return null;  // <-- Current behavior

    var stream = new MemoryStream();
    serializer.Serialize(value, stream);

    return stream.ToArray();
}

Options:

  1. Keep returning null (current behavior) - Allows callers to serialize nullable values without extra null checks
  2. Throw ArgumentNullException - More consistent with deserialization validation, fails fast
  3. Return empty array [] - Distinguishes "serialized null" from "no value", but could be confusing

@ejsmith What's your preference? The current behavior is documented but differs from the stricter validation on deserialization methods.

@niemyjski
niemyjski requested review from Copilot and ejsmith and removed request for Copilot January 21, 2026 22:11
@niemyjski niemyjski self-assigned this Jan 21, 2026
@niemyjski niemyjski changed the title Normalize ISerializer argument validation and enhance test coverage [Breaking]: Normalize ISerializer argument validation and enhance test coverage Jan 21, 2026
Performance:
- SystemTextJsonSerializer: Use direct stream APIs instead of Utf8JsonWriter/StreamReader
- SystemTextJsonSerializer: Fix DateTimeOffset parsing order (try DateTimeOffset before DateTime)
- SystemTextJsonSerializer: Improve number parsing (int32 → int64 → decimal → double)

Validation:
- Add ArgumentNullException validation to all four core ISerializer implementations
- Core Serialize: validates output stream (allows null value for serialization)
- Core Deserialize: validates data stream and objectType parameters

Breaking Change:
- SerializeToBytes(null) and SerializeToString(null) now return serialized 'null' literal
  instead of returning null (aligns with System.Text.Json behavior)

Tests:
- Add Serialize_WithInvalidArguments_ThrowsArgumentNullException
- Add Deserialize_WithInvalidArguments_ThrowsArgumentNullException
- Add Serialize_WithNullValue_RoundTripsCorrectly (consolidated null test)
- Add Serialize_WithSpecialCharacters_RoundTripsCorrectly (Unicode, emoji, escapes)
- Remove obsolete SerializeToBytes_WithNullValue_ReturnsNull
- Remove obsolete SerializeToString_WithNullValue_ReturnsNull
Replaces ArgumentException with ArgumentOutOfRangeException
for zero-length data validation in ISerializer extension methods.

This change streamlines argument validation by using the dedicated
ArgumentOutOfRangeException for checking data length, improving code
readability and consistency.
@niemyjski

Copy link
Copy Markdown
Member Author

Latest Commit: Serializer Performance, Validation, and Consistency Improvements

Performance Improvements (SystemTextJsonSerializer)

  1. Direct Stream APIs - Replaced Utf8JsonWriter with JsonSerializer.Serialize(stream, ...) and removed StreamReader.ReadToEnd() in favor of JsonSerializer.Deserialize(stream, ...) to avoid unnecessary string allocations.

  2. DateTimeOffset Order Fix - Now tries TryGetDateTimeOffset before TryGetDateTime to preserve timezone information.

  3. Improved Number Parsing - Order: int32 → int64 → decimal → double

    • Returns smallest appropriate type for optimal boxing
    • Preserves decimal precision (important for financial data)

Core Implementation Validation

Added ArgumentNullException.ThrowIfNull() to all four core ISerializer implementations for output stream in Serialize and data/objectType in Deserialize.

Note: value parameter is intentionally NOT validated - null values are valid and serialize to "null" literal.

Breaking Change ⚠️

SerializeToBytes(null) and SerializeToString(null) now return the serialized "null" literal instead of returning null. This aligns with System.Text.Json behavior.

New Tests

  • Serialize_WithInvalidArguments_ThrowsArgumentNullException
  • Deserialize_WithInvalidArguments_ThrowsArgumentNullException
  • Serialize_WithNullValue_RoundTripsCorrectly
  • Serialize_WithSpecialCharacters_RoundTripsCorrectly

Test Results: ✅ 84 serializer tests pass, 1763 total tests pass

…s with breaking changes

- Add comprehensive test for numeric primitive deserialization to object type
- Change ArgumentOutOfRangeException to ArgumentException for empty byte arrays to match documentation
- Update serialization.md with accurate null value behavior and breaking changes section
- All tests pass (1769 passed, 11 skipped)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 normalizes argument validation in the serializer abstraction layer to align with System.Text.Json behavior and significantly enhances test coverage. The changes introduce several breaking changes to improve consistency and correctness.

Changes:

  • Added argument validation to all SerializerExtensions methods using modern .NET throw helpers
  • Changed null value serialization behavior to return serialized representations instead of null
  • Enhanced SystemTextJsonSerializer primitive type handling to return more specific types (int instead of long, decimal instead of double in certain cases)
  • Expanded test suite from 3 to 15 tests per serializer implementation
  • Added comprehensive XML documentation to interfaces

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/Foundatio/Serializer/ISerializer.cs Added argument validation and XML documentation to all extension methods
src/Foundatio/Serializer/SystemTextJsonSerializer.cs Refactored to use direct stream serialization and improved primitive type coercion logic
src/Foundatio.JsonNet/JsonNetSerializer.cs Added argument validation and updated resource disposal pattern
src/Foundatio.MessagePack/MessagePackSerializer.cs Added argument validation to core methods
src/Foundatio.Utf8Json/Utf8JsonSerializer.cs Added argument validation to core methods
src/Foundatio.TestHarness/Serializer/SerializerTestsBase.cs Expanded from 3 to 15 test methods with comprehensive validation coverage
tests/Foundatio.Tests/Serializer/*.cs Updated all 6 concrete serializer test classes with full test method overrides
docs/guide/serialization.md Added documentation for extension methods and validation behavior

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Foundatio/Serializer/ISerializer.cs
Comment thread src/Foundatio/Serializer/ISerializer.cs
Comment thread src/Foundatio.TestHarness/Serializer/SerializerTestsBase.cs
Comment thread src/Foundatio/Serializer/SystemTextJsonSerializer.cs
Comment thread src/Foundatio/Serializer/ISerializer.cs
Comment thread src/Foundatio/Serializer/ISerializer.cs Outdated
Comment thread src/Foundatio/Serializer/SystemTextJsonSerializer.cs
Comment thread src/Foundatio.JsonNet/JsonNetSerializer.cs
Documents that ISerializer implementations should handle null values,
e.g., by serializing them as "null" or nil markers.
This ensures consistent behavior across different serializers.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants