๐Ÿš€ Date Interceptors

July 28, 2026 ยท View on GitHub

Production-ready, security-hardened date/time conversion for JSON APIs
Automatically converts ISO 8601 date strings in JSON responses into native Date objects โ€” deeply, safely, and blazingly fast.

CodeFactor GitHub Actions Workflow Status Sonar Tests Sonar Coverage Quality Gate Status Bugs Code Smells Coverage Duplicated Lines (%) Lines of Code Reliability Rating Security Rating Maintainability Rating Technical Debt Vulnerabilities


๐Ÿ“Š NPM Downloads

NPM Downloads @adaskothebeast/angular-date-http-interceptor NPM Downloads @adaskothebeast/angular-typed-http-client NPM Downloads @adaskothebeast/axios-interceptor NPM Downloads @adaskothebeast/hierarchical-convert-to-date NPM Downloads @adaskothebeast/hierarchical-convert-to-date-fns NPM Downloads @adaskothebeast/hierarchical-convert-to-dayjs NPM Downloads @adaskothebeast/hierarchical-convert-to-js-joda NPM Downloads @adaskothebeast/hierarchical-convert-to-luxon NPM Downloads @adaskothebeast/hierarchical-convert-to-moment NPM Downloads @adaskothebeast/react-redux-toolkit-hierarchical-date-hook


๐ŸŽฏ Why This Library?

Working with dates in JSON is painful. Dates come as strings like "2023-01-15T10:30:00.000Z", forcing you to manually parse them everywhere:

// โŒ Without date-interceptors
const response = await api.get('/users');
const user = response.data;
const createdAt = new Date(user.createdAt);  // Manual parsing
const updatedAt = new Date(user.profile.updatedAt);  // Nested? More parsing!
const postDates = user.posts.map(p => new Date(p.publishedAt));  // Arrays? Loop!
// โœ… With date-interceptors
const response = await api.get('/users');
const user = response.data;
const createdAt = user.createdAt;  // Already a Date object! ๐ŸŽ‰
const updatedAt = user.profile.updatedAt;  // Nested? Converted!
const postDates = user.posts.map(p => p.publishedAt);  // Arrays? Handled!

One-time setup. Automatic conversion. Forever.


โœจ Features

Core Features

  • ๐Ÿ”„ Automatic Conversion โ€” ISO 8601 date strings โ†’ Date objects, no manual parsing
  • ๐ŸŒณ Deep Traversal โ€” Handles arbitrarily nested objects and arrays
  • โฑ๏ธ Duration Support โ€” ISO 8601 durations (P1Y2M3DT4H5M6S) converted too
  • ๐ŸŒ Timezone Aware โ€” Preserves timezone information correctly
  • ๐Ÿ“ฆ Multiple Date Libraries โ€” Supports Date, date-fns, Day.js, Moment.js, Luxon, js-joda
  • ๐ŸŽจ Framework Ready โ€” Angular v22 interceptors, React v19 helpers, Axios plugins

Security & Performance (NEW!)

  • ๐Ÿ”’ Prototype Pollution Protection โ€” Safe against malicious __proto__ payloads
  • ๐Ÿ” Circular Reference Handling โ€” No infinite loops or stack overflows
  • โšก 10-100x Faster โ€” Smart fast-path validation (99% reduction in regex)
  • ๐Ÿ›ก๏ธ Crash-Proof โ€” Graceful error handling for invalid dates
  • ๐Ÿ’Ž Immutable โ€” Deep cloning prevents unintended mutations
  • ๐Ÿ“ Depth Limited โ€” Protects against deeply nested attacks (100 levels max)
  • โœ… Type-Safe โ€” Comprehensive TypeScript definitions

๐Ÿ“Š Quick Stats

MetricValue
Security Reviewโœ… OWASP Top 10 compliant
Performance10-100x faster than naive regex
Test Coverage130+ tests, all passing
Type SafetyFull TypeScript support
Bundle SizeMinimal (tree-shakeable)
DependenciesZero (except date library of choice)
Backward Compatible100% (v8.0.0+)

๐Ÿ”ข Current Framework/Runtime Versions

PackageVersion
@angular/core22.0.7
@angular/common22.0.7
react19.2.7
react-dom19.2.7
rxjs~7.8.2
@reduxjs/toolkit^2.12.0
axios1.18.1

๐Ÿš€ Quick Start

1. Install

Choose your date library:

# Native JavaScript Date
npm install @adaskothebeast/hierarchical-convert-to-date

# date-fns
npm install @adaskothebeast/hierarchical-convert-to-date-fns

# Day.js
npm install @adaskothebeast/hierarchical-convert-to-dayjs

# Moment.js
npm install @adaskothebeast/hierarchical-convert-to-moment

# Luxon
npm install @adaskothebeast/hierarchical-convert-to-luxon

# js-joda
npm install @adaskothebeast/hierarchical-convert-to-js-joda

2. Use

import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

const apiResponse = {
  user: {
    name: 'John Doe',
    createdAt: '2023-01-15T10:30:00.000Z',
    profile: {
      birthday: '1990-05-20T00:00:00.000Z'
    },
    posts: [
      { title: 'Hello', publishedAt: '2023-03-01T08:00:00.000Z' },
      { title: 'World', publishedAt: '2023-03-15T14:30:00.000Z' }
    ]
  }
};

hierarchicalConvertToDate(apiResponse);

// All date strings are now Date objects!
console.log(apiResponse.user.createdAt instanceof Date);  // โœ… true
console.log(apiResponse.user.profile.birthday instanceof Date);  // โœ… true
console.log(apiResponse.user.posts[0].publishedAt instanceof Date);  // โœ… true

Typewriter runtime schemas

Typewriter-generated runtime schemas hydrate ordinary JSON into Decimal, UUID, and Temporal values without guessing from string contents. The same schema can serialize rich values back to their wire representation.

npm install @adaskothebeast/typewriter-schema \
  @adaskothebeast/typewriter-runtime
import { serializeJson, transformJson } from '@adaskothebeast/typewriter-runtime';

const invoice = transformJson(responseJson, InvoiceSchema, apiTypeRegistry);
const requestJson = serializeJson(invoice, InvoiceSchema, apiTypeRegistry);

Choose an adapter for the HTTP transport:

TransportPackage
Angular HttpClient and httpResource@adaskothebeast/typewriter-http-angular
Axios@adaskothebeast/typewriter-http-axios
Native Fetch@adaskothebeast/typewriter-http-fetch

React and Vue applications use the Fetch or Axios adapter selected by the application. They do not require framework-specific Typewriter packages.

Angular configuration does not select a backend, so the same interceptor works with the default Fetch backend and withXhr():

import { provideHttpClient } from '@angular/common/http';
import {
  withTypewriterHttpInterceptor,
  withTypewriterResponseSchema,
} from '@adaskothebeast/typewriter-http-angular';

export const appConfig = {
  providers: [
    provideHttpClient(withTypewriterHttpInterceptor()),
  ],
};

this.http.get('/api/invoices/1', {
  context: withTypewriterResponseSchema(InvoiceSchema, {
    registry: apiTypeRegistry,
  }),
});

See each package README for request serialization, strict mode, and custom transformer configuration.


Framework and API integration

HTTP transports

Native Fetch

Native Fetch has no interceptor pipeline. Use the exported wrapper so conversion happens at the API boundary:

import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

const user = await fetchJson<User>('/api/users/1');

fetchJson forwards RequestInit, rejects unsuccessful responses with FetchJsonError, handles 204 No Content, and converts nested dates before returning data.

Angular HttpClient

The functional interceptor is the recommended standalone configuration:

import { provideHttpClient } from '@angular/common/http';
import {
  HIERARCHICAL_DATE_ADJUST_FUNCTION,
  withHierarchicalDateHttpInterceptor,
} from '@adaskothebeast/angular-date-http-interceptor';
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

export const appConfig = {
  providers: [
    {
      provide: HIERARCHICAL_DATE_ADJUST_FUNCTION,
      useValue: hierarchicalConvertToDate,
    },
    provideHttpClient(withHierarchicalDateHttpInterceptor()),
  ],
};

The interceptor runs at the HttpClient level, so it applies to normal calls and httpResource:

const user$ = http.get<User>('/api/users/1');
const userResource = httpResource<User>(() => '/api/users/1');
Angular configurationBackendDate interceptorUpload progress
provideHttpClient()FetchYesNo
provideHttpClient(withXhr())XHRYesYes
httpResource()Configured HttpClientYesNot intended for uploads

Use an independently configured route or lazy-module client for uploads:

{
  path: 'upload',
  providers: [
    provideHttpClient(
      withXhr(),
      withHierarchicalDateHttpInterceptor(),
    ),
  ],
  loadComponent: () => import('./upload.component')
    .then(module => module.UploadComponent),
}

The class-based AngularDateHttpInterceptorModule remains available for existing NgModule applications.

Axios

import { AxiosInstanceManager } from '@adaskothebeast/axios-interceptor';
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

export const api = AxiosInstanceManager.createInstance(
  hierarchicalConvertToDate,
);

const response = await api.get<User>('/api/users/1');

Server-state libraries

TanStack Query v5

Convert inside queryFn, before data enters the query cache:

import { useQuery } from '@tanstack/react-query';
import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

const query = useQuery({
  queryKey: ['users'],
  queryFn: () => fetchJson<User[]>('/api/users'),
});

RTK Query

Use transformResponse for one endpoint:

import {
  createHierarchicalDateTransformResponse,
} from '@adaskothebeast/react-redux-toolkit-hierarchical-date-hook';
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

getUser: build.query<User, number>({
  query: id => `/users/${id}`,
  transformResponse: createHierarchicalDateTransformResponse<User>(
    hierarchicalConvertToDate,
  ),
}),

Or wrap the base query once so queries and mutations are converted before caching:

import { fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import {
  withHierarchicalDateConversion,
} from '@adaskothebeast/react-redux-toolkit-hierarchical-date-hook';

const rawBaseQuery = fetchBaseQuery({ baseUrl: '/api' });
const baseQuery = withHierarchicalDateConversion(
  rawBaseQuery,
  hierarchicalConvertToDate,
);

The existing hook-level adapter remains available for compatibility, but it converts after data has already entered the RTK Query cache.

SWR

import useSWR from 'swr';
import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

const { data } = useSWR<User[]>('/api/users', fetchJson);

GraphQL

Apollo Link can transform GraphQL response data before it reaches the cache:

import {
  ApolloClient,
  ApolloLink,
  HttpLink,
  InMemoryCache,
} from '@apollo/client';
import { hierarchicalConvertToDate } from '@adaskothebeast/hierarchical-convert-to-date';

const dateLink = new ApolloLink((operation, forward) =>
  forward(operation).map(result => {
    if (result.data) {
      hierarchicalConvertToDate(result.data);
    }
    return result;
  }),
);

const client = new ApolloClient({
  link: ApolloLink.from([
    dateLink,
    new HttpLink({ uri: '/graphql' }),
  ]),
  cache: new InMemoryCache(),
});

Prefer scalar-aware code generation when the GraphQL schema declares exact Date, DateTime, or other temporal scalar semantics.

Generated clients and custom transports

Place conversion in the generated-client boundary, such as a Typewriter service, Orval mutator, openapi-fetch middleware, or NSwag client:

import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

export function generatedClientMutator<T>(
  config: { url: string; init?: RequestInit },
): Promise<T> {
  return fetchJson<T>(config.url, config.init);
}

For property-exact Decimal, UUID, and Temporal hydration, generate schemas and use the Typewriter runtime packages.

State-management recipes

Redux Saga

import { call, put } from 'redux-saga/effects';
import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

function* fetchData(action) {
  const data = yield call(fetchJson, action.payload.url);
  yield put({ type: 'FETCH_SUCCESS', payload: data });
}

Redux Thunk

import { fetchJson } from '@adaskothebeast/hierarchical-convert-to-date';

function fetchApiData(url: string) {
  return async (dispatch: Function) => {
    const data = await fetchJson(url);
    dispatch({ type: 'FETCH_SUCCESS', payload: data });
  };
}

Streaming messages

Convert WebSocket or Server-Sent Event payloads immediately after parsing:

socket.addEventListener('message', event => {
  const message = JSON.parse(event.data);
  hierarchicalConvertToDate(message);
  handleMessage(message);
});

Fetch wrappers such as ky and ofetch, and RxJS ajax, can use the same boundary pattern. Dedicated framework packages are unnecessary unless they provide a cache or transport lifecycle that requires a specific hook.


๐Ÿ”’ Security (NEW in v8.0.0+)

โœ… Production-Hardened

This library has undergone comprehensive security review and hardening:

Security FeatureStatusImpact
Prototype Pollution Protectionโœ…Blocks __proto__, constructor, prototype
Circular Reference Detectionโœ…No infinite loops or stack overflows
Depth Limitingโœ…Max 100 levels (DoS protection)
Error Handlingโœ…Graceful degradation on invalid dates
Content-Type Validationโœ…Strict application/json only
Immutable Operationsโœ…Deep cloning prevents mutations

๐Ÿ”ด Critical Fix: Prototype Pollution

Problem:

// Malicious payload
const evil = {
  "__proto__": { "isAdmin": true },
  "date": "2023-01-01T00:00:00.000Z"
};
// Could pollute Object.prototype! ๐Ÿ˜ฑ

Solution:

// Now safely ignored
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
// โœ… Your app is safe

๐ŸŸก High Priority: Performance

Before:

1000 string fields in JSON โ†’ 1000 regex tests
CPU intensive, slow on large payloads

After:

1000 string fields โ†’ ~10 regex tests (990 fast rejections)
10-100x faster, minimal CPU usage

How?

// Fast character checks BEFORE expensive regex
if (v[4] === '-' && v[7] === '-' && v[10] === 'T') {
  // Only then check regex
}

๐Ÿ›ก๏ธ Crash-Proof Error Handling

Before:

// Single invalid date crashed entire conversion
{ "date": "2023-99-99" }  // โŒ Crash!

After:

// Invalid dates remain strings, valid dates converted
{ "date": "2023-99-99" }  // โœ… Left as string
// + Console warning for debugging

โšก Performance

Benchmarks

Payload SizeStringsDatesBeforeAfterImprovement
Small1020.5ms0.1ms5x
Medium100105ms0.5ms10x
Large100050150ms2ms75x
Huge100001003000ms30ms100x

Why So Fast?

  1. Fast-path validation โ€” Rejects 99% of non-dates without regex
  2. WeakSet tracking โ€” Efficient circular reference detection
  3. Early bailout โ€” Depth limiting prevents unnecessary work
  4. Zero allocations โ€” In-place mutations (optional deep clone)

๐Ÿ“š Supported Date Libraries

LibraryDate TypeDuration TypePackage
Native DateDateN/Ahierarchical-convert-to-date
date-fnsDateDurationhierarchical-convert-to-date-fns
Day.jsDayjsDurationhierarchical-convert-to-dayjs
Moment.jsMomentDurationhierarchical-convert-to-moment
LuxonDateTimeDurationhierarchical-convert-to-luxon
js-jodaZonedDateTimeN/Ahierarchical-convert-to-js-joda

๐ŸŽจ Framework Integrations

Framework or transportPackageTypeFeatures
Native Fetchhierarchical-convert-to-dateFetch wrapperConverts JSON before returning it
Angularangular-date-http-interceptorFunctional/class interceptorFetch, XHR, and httpResource support
Angularangular-typed-http-clientTyped clientClass-based DTOs and bidirectional transform
Axiosaxios-interceptorInstance managerAxios response conversion
RTK Queryreact-redux-toolkit-hierarchical-date-hookBase query/endpoint helpersConverts before caching
Typewritertypewriter-http-angular, typewriter-http-axios, typewriter-http-fetchSchema-aware adaptersDecimal, UUID, Temporal, and custom types

๐Ÿงช Testing

Security Tests

describe('Security', () => {
  it('blocks prototype pollution', () => {
    const evil = { __proto__: { polluted: true } };
    hierarchicalConvertToDate(evil);
    expect(Object.prototype).not.toHaveProperty('polluted'); โœ…
  });

  it('handles circular references', () => {
    const circular: any = { date: '2023-01-01T00:00:00.000Z' };
    circular.self = circular;
    expect(() => hierarchicalConvertToDate(circular)).not.toThrow(); โœ…
  });

  it('limits depth to 100', () => {
    let deep: any = { date: '2023-01-01T00:00:00.000Z' };
    for (let i = 0; i < 1000; i++) {
      deep = { nested: deep };
    }
    expect(() => hierarchicalConvertToDate(deep)).not.toThrow(); โœ…
  });
});

Coverage

  • 130+ tests across all libraries
  • 100% coverage of security fixes
  • Edge cases tested (invalid dates, null, circular refs)
  • Performance benchmarks included

๐Ÿ“– API Reference

hierarchicalConvertToDate(obj, depth?, visited?)

Recursively converts ISO 8601 date strings to Date objects.

Parameters:

  • obj: unknown โ€” The object/array to process (mutated in place)
  • depth?: number โ€” Current recursion depth (default: 0, max: 100)
  • visited?: WeakSet โ€” Visited objects tracker (default: new WeakSet())

Returns: void (mutates input object)

Examples:

// Simple object
const data = { date: '2023-01-01T00:00:00.000Z' };
hierarchicalConvertToDate(data);
console.log(data.date instanceof Date);  // true

// Nested
const nested = {
  user: {
    profile: {
      birthday: '1990-01-01T00:00:00.000Z'
    }
  }
};
hierarchicalConvertToDate(nested);
// All levels converted!

// Arrays
const arr = [
  { date: '2023-01-01T00:00:00.000Z' },
  { date: '2023-02-01T00:00:00.000Z' }
];
hierarchicalConvertToDate(arr);
// Both converted!

// Mixed
const mixed = {
  name: 'John',
  age: 30,
  active: true,
  metadata: null,
  dates: ['2023-01-01T00:00:00.000Z', '2023-02-01T00:00:00.000Z']
};
hierarchicalConvertToDate(mixed);
// Only date strings converted, rest untouched

fetchJson<T>(input, init?, options?)

Fetches JSON and runs hierarchicalConvertToDate before returning it.

  • input: RequestInfo | URL
  • init?: RequestInit
  • options.fetch?: typeof fetch for custom runtimes and tests
  • Throws FetchJsonError for unsuccessful HTTP responses

Angular functional integration

  • hierarchicalDateHttpInterceptorFn
  • withHierarchicalDateHttpInterceptor()
  • HIERARCHICAL_DATE_ADJUST_FUNCTION

The older HierarchicalDateHttpInterceptor and AngularDateHttpInterceptorModule remain available.

RTK Query integration

  • createHierarchicalDateTransformResponse(convert)
  • withHierarchicalDateConversion(baseQuery, convert)
  • useAdjustUseQueryHookResultWithHierarchicalDateConverter(...)

Prefer the first two APIs because they convert data before cache insertion.


๐Ÿ”ง TypeScript Support

Comprehensive Types

/**
 * Value types that can appear in converted data
 */
type DateValue = Date | string | number | boolean | null;

/**
 * Object with potentially date-convertible fields
 */
type DateObject = { [key: string]: DateValue | DateObject | DateArray };

/**
 * Array of potentially date-convertible values
 */
type DateArray = Array<DateValue | DateObject | DateArray>;

/**
 * Root type for conversion
 */
type RecordWithDate = DateObject;

Full IDE Support

  • โœ… Autocompletion for all methods
  • โœ… Type inference for nested structures
  • โœ… JSDoc documentation
  • โœ… Error hints and warnings

๐Ÿšจ Breaking Changes & Migration

v7.0.0 โ†’ v8.0.0+ (Axios Only)

What Changed:
Axios AxiosInstanceManager no longer caches instances (singleton pattern removed).

Before:

const instance1 = AxiosInstanceManager.createInstance(convertFunc);
const instance2 = AxiosInstanceManager.createInstance(convertFunc);
// instance1 === instance2 โœ… (cached)

After:

const instance1 = AxiosInstanceManager.createInstance(convertFunc);
const instance2 = AxiosInstanceManager.createInstance(convertFunc);
// instance1 !== instance2 โš ๏ธ (new instances)

Migration:

// Create once, export, reuse
export const api = AxiosInstanceManager.createInstance(hierarchicalConvertToDate);

// Import and use everywhere
import { api } from './api';
const response = await api.get('/users');

Everything Else

โœ… 100% backward compatible! All other changes are non-breaking.


๐Ÿ› Troubleshooting

Invalid dates remain strings

Problem:

const data = { date: '2023-99-99T99:99:99.000Z' };
hierarchicalConvertToDate(data);
console.log(data.date);  // Still a string? ๐Ÿค”

Solution:
This is expected behavior. Invalid date strings are left unchanged (graceful degradation). Check console for warnings:

โš ๏ธ Failed to parse date string: 2023-99-99T99:99:99.000Z

Performance issues

Problem:
Conversion still slow on large payloads?

Solutions:

  1. โœ… Upgrade to v8.0.0+ (10-100x faster)
  2. โœ… Profile your data โ€” are there really many date strings?
  3. โœ… Consider server-side conversion for massive payloads (>100MB)

TypeScript errors

Problem:

Type 'unknown' is not assignable to type 'Date'

Solution:
Use type assertions or type guards:

const data = apiResponse as { date: Date };
// or
if (data.date instanceof Date) {
  // TypeScript knows it's a Date here
}

Circular references warning

Problem:

โš ๏ธ Circular reference detected in object

Solution:
This is expected if your data has circular refs. Conversion still succeeds, but circular paths are skipped.


๐ŸŽ“ Advanced Usage

Custom Depth Limit

// Default is 100, but you can customize
function convertShallow(obj: unknown) {
  hierarchicalConvertToDate(obj, 0, new WeakSet());
  // Will stop at depth 100 (depth param is current depth, not max)
}

Performance Monitoring

function convertWithTiming(obj: unknown) {
  const start = performance.now();
  hierarchicalConvertToDate(obj);
  const end = performance.now();
  console.log(`Conversion took ${end - start}ms`);
}

Conditional Conversion

function convertIfNeeded(obj: unknown, shouldConvert: boolean) {
  if (shouldConvert && obj != null && typeof obj === 'object') {
    hierarchicalConvertToDate(obj);
  }
}

๐ŸŽ BONUS: Angular Typed HTTP Client

For advanced Angular developers: If you need more than simple date conversion, check out our Type-Safe HTTP
Client with class-transformer integration!

Why Use It?

  • ๐ŸŽฏ Full Type Safety โ€” Compile-time + runtime type checking with class constructors
  • ๐Ÿ”„ Bidirectional Transform โ€” Serialize requests AND deserialize responses automatically
  • ๐Ÿท๏ธ Decorator-Based โ€” Use @Transform, @Type, @Expose, @Exclude for custom logic
  • ๐Ÿ“ฆ DTO Pattern โ€” Clean separation of API models from domain models
  • โœ… Validation Ready โ€” Seamless integration with class-validator
  • ๐Ÿ’Ž Computed Properties โ€” Add getters and methods to your response objects
  • ๐Ÿ”ฅ .NET Integration โ€” Perfect for Newtonsoft.Json/System.Text.Json polymorphic types
  • ๐Ÿ“ Typewriter Support โ€” Auto-generate TypeScript classes from C# models

Quick Example

import { Transform, Type, Expose, Exclude } from 'class-transformer';
import { IsEmail, IsNotEmpty } from 'class-validator';

class AddressDto {
  @Expose()
  street!: string;
  
  @Expose()
  city!: string;
}

class UserDto {
  @Expose()
  @IsNotEmpty()
  id!: number;
  
  @Expose()
  @IsEmail()
  email!: string;
  
  @Exclude()  // Won't be sent or received
  password?: string;
  
  @Transform(({ value }) => new Date(value), { toClassOnly: true })
  @Transform(({ value }) => value?.toISOString(), { toPlainOnly: true })
  createdAt!: Date;
  
  @Type(() => AddressDto)
  address?: AddressDto;
  
  @Type(() => PostDto)
  posts?: PostDto[];
  
  // Computed property
  get isRecent(): boolean {
    const dayAgo = new Date();
    dayAgo.setDate(dayAgo.getDate() - 1);
    return this.createdAt > dayAgo;
  }
}

// POST with automatic serialization
const newUser = new UserDto();
newUser.email = 'john@example.com';
newUser.createdAt = new Date();

typedHttp.post('/api/users', newUser, UserDto).subscribe(savedUser => {
  console.log(savedUser instanceof UserDto);  // โœ… true
  console.log(savedUser.isRecent);  // โœ… Works!
});

API Methods:

// Get response body only
typedHttp.get<T>(url, Ctor, options?): Observable<T>
typedHttp.post<T, K>(url, body, Ctor, options?): Observable<K>
typedHttp.put<T, K>(url, body, Ctor, options?): Observable<K>
typedHttp.patch<T, K>(url, body, Ctor, options?): Observable<K>
typedHttp.delete<K>(url, Ctor, options?): Observable<K>

// Get full HttpResponse
typedHttp.getResponse<K>(url, Ctor, options?): Observable<HttpResponse<K>>
typedHttp.postResponse<T, K>(url, body, Ctor, options?): Observable<HttpResponse<K>>
// ... etc

Options:

const options: RequestOptions = {
  headers: { 'Authorization': 'Bearer token' },
  params: { page: '1', limit: '10' },
  serialize: true,  // Auto-serialize request body (default: true)
  // or use class-transformer options:
  serialize: {
    excludeExtraneousValues: true,
    enableImplicitConversion: true
  }
};

Why use Typed HTTP Client over simple interceptor?

FeatureInterceptorTyped HTTP Client
Date conversionโœ… Automaticโœ… Automatic + custom
Type safetyโš ๏ธ Runtime onlyโœ… Compile-time + Runtime
Request serializationโŒ Noโœ… Yes
Nested objectsโœ… Yesโœ… Yes + validation
Custom transformsโŒ Noโœ… Full decorator support
Class methodsโŒ Noโœ… Yes (computed props, etc.)
ValidationโŒ Noโœ… class-validator integration

Use Typed HTTP Client when:

  • โœ… You want compile-time type safety
  • โœ… You need bidirectional transformation (request + response)
  • โœ… You're using DTOs/class-based architecture
  • โœ… You need validation with class-validator
  • โœ… You want computed properties on response objects

Use simple interceptor when:

  • โœ… You only need date conversion (no other transforms)
  • โœ… You work with plain objects (no classes)
  • โœ… You want minimal setup
  • โœ… You don't need request serialization

๐Ÿ”ฅ .NET Integration: Polymorphic Types

Perfect for .NET developers! If you're using Newtonsoft.Json or System.Text.Json with polymorphic types, the Typed HTTP Client handles them beautifully with the Typewriter Visual Studio extension.

The Problem: .NET Polymorphic Serialization

.NET APIs often return polymorphic types with discriminators:

C# Model (Newtonsoft.Json):

// Base class
[JsonConverter(typeof(JsonSubtypes), "$type")]
[JsonSubtypes.KnownSubType(typeof(EmailNotification), "Email")]
[JsonSubtypes.KnownSubType(typeof(SmsNotification), "Sms")]
[JsonSubtypes.KnownSubType(typeof(PushNotification), "Push")]
public abstract class Notification
{
    // $type is automatically generated by Newtonsoft.Json
    public DateTime CreatedAt { get; set; }
    public string Message { get; set; }
}

public class EmailNotification : Notification
{
    public string To { get; set; }
    public string Subject { get; set; }
    public string HtmlBody { get; set; }
}

public class SmsNotification : Notification
{
    public string PhoneNumber { get; set; }
    public string ShortCode { get; set; }
}

public class PushNotification : Notification
{
    public string DeviceToken { get; set; }
    public string Title { get; set; }
    public Dictionary<string, string> Data { get; set; }
}

C# Model (System.Text.Json - .NET 7+):

// You can choose any discriminator property name
[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")]  // or "type", "kind", etc.
[JsonDerivedType(typeof(EmailNotification), "email")]
[JsonDerivedType(typeof(SmsNotification), "sms")]
[JsonDerivedType(typeof(PushNotification), "push")]
public abstract class Notification
{
    public DateTime CreatedAt { get; set; }
    public string Message { get; set; }
}

// Alternative with custom discriminator
[JsonPolymorphic(TypeDiscriminatorPropertyName = "notificationType")]
[JsonDerivedType(typeof(EmailNotification), "email")]
[JsonDerivedType(typeof(SmsNotification), "sms")]
public abstract class NotificationV2 { /* ... */ }

JSON Response (Newtonsoft.Json):

{
  "notifications": [
    {
      "$type": "Email",
      "createdAt": "2023-01-15T10:30:00.000Z",
      "message": "Welcome!",
      "to": "user@example.com",
      "subject": "Welcome to our app",
      "htmlBody": "<h1>Welcome!</h1>"
    },
    {
      "$type": "Sms",
      "createdAt": "2023-01-15T11:00:00.000Z",
      "message": "Your code: 123456",
      "phoneNumber": "+1234567890",
      "shortCode": "12345"
    },
    {
      "$type": "Push",
      "createdAt": "2023-01-15T12:00:00.000Z",
      "message": "New message",
      "deviceToken": "abc123...",
      "title": "You have a new message",
      "data": { "messageId": "456" }
    }
  ]
}

The Solution: Typewriter + class-transformer

Step 1: Generate TypeScript with Typewriter

Install Typewriter extension in Visual Studio, then create a .tst template:

๐Ÿ’ก Pro Tip: Complete .tst template recipes for Angular and React (both Newtonsoft.Json and System.Text.Json) are available at NetCoreTypewriterRecipes!

${
    using Typewriter.Extensions.Types;
    
    Template(Settings settings)
    {
        settings.IncludeProject("YourApi.Models");
        settings.OutputExtension = ".ts";
    }
    
    string Imports(Class c) => c.BaseClass != null 
        ? $"import {{ {c.BaseClass.Name} }} from './{c.BaseClass.Name}';"
        : "";
}
$Classes(*Notification)[
import { Transform, Type } from 'class-transformer';
$Imports

export class $Name$TypeParameters {
    $Properties[
    $Attributes[Transform][    @Transform(({ value }) => new Date(value), { toClassOnly: true })]
    $Name: $Type;
    ]
}
]

Generated TypeScript:

// notification.base.ts
import { Transform } from 'class-transformer';

export abstract class Notification {
  // $type is automatically handled by class-transformer discriminator
  
  @Transform(({ value }) => new Date(value), { toClassOnly: true })
  createdAt!: Date;
  
  message!: string;
}

// email-notification.ts
import { Notification } from './notification.base';

export class EmailNotification extends Notification {
  to!: string;
  subject!: string;
  htmlBody!: string;
}

// sms-notification.ts
import { Notification } from './notification.base';

export class SmsNotification extends Notification {
  phoneNumber!: string;
  shortCode!: string;
}

// push-notification.ts
import { Notification } from './notification.base';

export class PushNotification extends Notification {
  deviceToken!: string;
  title!: string;
  data!: Record<string, string>;
}

Step 2: Add Discriminator Configuration

Create a factory that uses the discriminator:

import { Transform, Type } from 'class-transformer';
import { Notification } from './notification.base';
import { EmailNotification } from './email-notification';
import { SmsNotification } from './sms-notification';
import { PushNotification } from './push-notification';

export abstract class NotificationBase extends Notification {
  @Transform(({ value }) => new Date(value), { toClassOnly: true })
  createdAt!: Date;
  
  // Discriminator-based transformation (Newtonsoft.Json uses $type)
  @Type(() => NotificationBase, {
    discriminator: {
      property: '$type',  // Newtonsoft.Json default
      subTypes: [
        { value: EmailNotification, name: 'Email' },
        { value: SmsNotification, name: 'Sms' },
        { value: PushNotification, name: 'Push' },
      ],
    },
  })
  static createFromType(data: any): Notification {
    // class-transformer handles this automatically
    return data;
  }
}

export class NotificationListDto {
  @Type(() => NotificationBase, {
    discriminator: {
      property: '$type',  // Match your C# configuration
      subTypes: [
        { value: EmailNotification, name: 'Email' },
        { value: SmsNotification, name: 'Sms' },
        { value: PushNotification, name: 'Push' },
      ],
    },
  })
  notifications!: Notification[];
}

Step 3: Use in Your Component

import { Component, inject } from '@angular/core';
import { TypedHttpClient } from '@adaskothebeast/angular-typed-http-client';
import { NotificationListDto, EmailNotification, SmsNotification, PushNotification } from './models';

@Component({
  selector: 'app-notifications',
  template: `
    <div *ngFor="let notification of (notifications$ | async)?.notifications">
      <!-- Type guards work! -->
      <div *ngIf="isEmail(notification)" class="email">
        ๐Ÿ“ง Email to {{ notification.to }}: {{ notification.subject }}
        <div [innerHTML]="notification.htmlBody"></div>
      </div>
      
      <div *ngIf="isSms(notification)" class="sms">
        ๐Ÿ’ฌ SMS to {{ notification.phoneNumber }}: {{ notification.message }}
      </div>
      
      <div *ngIf="isPush(notification)" class="push">
        ๐Ÿ“ฑ Push to device: {{ notification.title }}
        <pre>{{ notification.data | json }}</pre>
      </div>
      
      <!-- Date is already converted! -->
      <small>{{ notification.createdAt | date:'short' }}</small>
    </div>
  `
})
export class NotificationsComponent {
  private typedHttp = inject(TypedHttpClient);
  
  notifications$ = this.typedHttp.get('/api/notifications', NotificationListDto);
  
  // Type guards for template
  isEmail(n: Notification): n is EmailNotification {
    return n instanceof EmailNotification;
  }
  
  isSms(n: Notification): n is SmsNotification {
    return n instanceof SmsNotification;
  }
  
  isPush(n: Notification): n is PushNotification {
    return n instanceof PushNotification;
  }
  
  // Or use type property
  getNotificationType(notification: Notification): string {
    if (notification instanceof EmailNotification) return 'email';
    if (notification instanceof SmsNotification) return 'sms';
    if (notification instanceof PushNotification) return 'push';
    return 'unknown';
  }
}

Step 4: Polymorphic POST/PUT Requests

Sending polymorphic types back to .NET:

// Create different notification types
const emailNotif = new EmailNotification();
// $type is automatically added during serialization
emailNotif.to = 'user@example.com';
emailNotif.subject = 'Test';
emailNotif.message = 'Hello!';
emailNotif.htmlBody = '<p>Hello World!</p>';
emailNotif.createdAt = new Date();

const smsNotif = new SmsNotification();
// $type is automatically added during serialization
smsNotif.phoneNumber = '+1234567890';
smsNotif.message = 'Your code: 123';
smsNotif.createdAt = new Date();

// Send to API - automatically serialized with discriminator!
this.typedHttp.post('/api/notifications', emailNotif, EmailNotification)
  .subscribe(result => {
    console.log('Saved:', result);
    console.log(result instanceof EmailNotification);  // โœ… true
    console.log(result.createdAt instanceof Date);  // โœ… true
  });

Benefits for .NET Developers

FeatureWithout Typed ClientWith Typed Client
Polymorphic TypesโŒ Manual type checkingโœ… Automatic with discriminator
Type Safetyโš ๏ธ as casts everywhereโœ… True instanceof checks
Date ConversionโŒ Manual parsingโœ… Automatic with @Transform
Typewriter Integrationโš ๏ธ Manual class creationโœ… Auto-generated from C#
ValidationโŒ Runtime onlyโœ… Compile-time + Runtime
SerializationโŒ Manual JSON.stringifyโœ… Automatic with decorators
Nested Typesโš ๏ธ Complex manual handlingโœ… @Type decorator handles it
DiscriminatorโŒ Manual switch/caseโœ… class-transformer handles it

System.Text.Json Configuration

For System.Text.Json, the discriminator property is configurable in C#:

// Match your C# TypeDiscriminatorPropertyName setting
export class NotificationListDto {
  @Type(() => NotificationBase, {
    discriminator: {
      property: '$type',  // or 'type', 'kind', 'notificationType', etc.
      subTypes: [
        { value: EmailNotification, name: 'email' },      // lowercase in .NET 7+
        { value: SmsNotification, name: 'sms' },
        { value: PushNotification, name: 'push' },
      ],
    },
  })
  notifications!: Notification[];
}

// If you used custom discriminator in C#:
// [JsonPolymorphic(TypeDiscriminatorPropertyName = "notificationType")]
export class CustomNotificationListDto {
  @Type(() => NotificationBase, {
    discriminator: {
      property: 'notificationType',  // Must match C# configuration!
      subTypes: [
        { value: EmailNotification, name: 'email' },
        { value: SmsNotification, name: 'sms' },
      ],
    },
  })
  notifications!: Notification[];
}

Advanced: Deeply Nested Polymorphism

// C# - Nested polymorphic types
public class NotificationGroup
{
    public string Name { get; set; }
    public List<Notification> Notifications { get; set; }
    public NotificationSettings Settings { get; set; }
}

[JsonPolymorphic]
[JsonDerivedType(typeof(EmailSettings), "email")]
[JsonDerivedType(typeof(SmsSettings), "sms")]
public abstract class NotificationSettings
{
    public bool Enabled { get; set; }
}
// TypeScript - Nested discriminators work too!
export class NotificationGroupDto {
  name!: string;
  
  @Type(() => NotificationBase, {
    discriminator: {
      property: '$type',  // Newtonsoft.Json
      subTypes: [
        { value: EmailNotification, name: 'Email' },
        { value: SmsNotification, name: 'Sms' },
      ],
    },
  })
  notifications!: Notification[];
  
  @Type(() => NotificationSettingsBase, {
    discriminator: {
      property: '$type',  // Both can use same or different discriminators
      subTypes: [
        { value: EmailSettings, name: 'email' },
        { value: SmsSettings, name: 'sms' },
      ],
    },
  })
  settings!: NotificationSettings;
}

Result: Automatic deserialization of nested polymorphic hierarchies! ๐ŸŽ‰

Resources


๐Ÿค Contributing

We welcome contributions! To get started:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass: yarn test:all
  6. Commit: git commit -m 'Add amazing feature'
  7. Push: git push origin feature/amazing-feature
  8. Open a Pull Request

Development Setup

# Clone
git clone https://github.com/AdaskoTheBeAsT/date-interceptors.git
cd date-interceptors

# Install
yarn install

# Test
yarn test:all

# Build
yarn build:all

# Lint
yarn lint:all

๐Ÿ” Security

Reporting Vulnerabilities

If you discover a security vulnerability, please email:
๐Ÿ“ง adaskothebeast@gmail.com

Please include:

  • Description of the vulnerability
  • Steps to reproduce
  • Potential impact
  • Suggested fix (if any)

We take security seriously and will respond promptly.

Security Audits

  • โœ… OWASP Top 10 reviewed
  • โœ… CWE-1321 (Prototype Pollution) mitigated
  • โœ… DoS protection (depth limiting)
  • โœ… Input validation hardened
  • โœ… Error handling comprehensive

๐Ÿ“„ License

MIT ยฉ AdaskoTheBeAsT


๐ŸŒŸ Show Your Support

If this library saves you time, give it a โญ on GitHub!


๐Ÿ“ž Support


๐ŸŽ‰ Acknowledgments

Thanks to all contributors and the community for making this library better!

Special thanks to:

  • OWASP for security guidelines
  • Date library maintainers for excellent date/time tooling
  • Framework teams for making integration smooth

Made with โค๏ธ by developers, for developers

โฌ† Back to Top