npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@adaskothebeast/hierarchical-convert-to-temporal

v10.0.2

Published

**Deep, in-place conversion of ISO 8601 strings inside JSON payloads into `Temporal` values, plus a ready-made `Temporal` backend for the schema-driven typewriter runtime, part of [date-interceptors](https://github.com/AdaskoTheBeAsT/date-interceptors).**

Readme

⏳ @adaskothebeast/hierarchical-convert-to-temporal

Deep, in-place conversion of ISO 8601 strings inside JSON payloads into Temporal values, plus a ready-made Temporal backend for the schema-driven typewriter runtime, part of date-interceptors.

npm license

Peer dependencies: @js-temporal/polyfill ^0.5.1, @adaskothebeast/typewriter-runtime 10.0.0 (types only, used by the backend export) and tslib ^2.8.1. Version 10.0.0.


📦 Install

npm i @adaskothebeast/hierarchical-convert-to-temporal @js-temporal/polyfill @adaskothebeast/typewriter-runtime

@js-temporal/polyfill is imported directly by this package (import { Temporal } from '@js-temporal/polyfill'), so it is required even on runtimes that already ship a native Temporal.


🎯 What it does

Give it a parsed JSON response and it walks every own enumerable property of every nested object and array, replacing matching strings with Temporal instances. Nothing is returned; the input graph is mutated in place.

| String shape | Becomes | | --------------------------------------- | -------------------------- | | 2023-07-17T23:06:00.000Z | Temporal.Instant | | 2023-07-17T23:06:00.000+01:00 | Temporal.Instant | | 2023-07-17T23:06:00 (no offset) | Temporal.PlainDateTime | | P1Y2M4DT2H3M2S, -PT1H, PT0.5S | Temporal.Duration | | anything else | left untouched |

Only three Temporal types are ever produced by the walker: Instant, PlainDateTime and Duration. Date-only, time-only, year-month, month-day and zoned ([Europe/Paris]) strings are not recognised, because a bare JSON string carries no hint about which of those you meant. When you need the full set, use the schema-driven path with temporalDateBackend (see below).


🧰 API

| Export | Signature | Notes | | ------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | | hierarchicalConvertToTemporal | (obj: unknown, depth?: number, visited?: WeakSet<object>) => void | Mutates obj in place, returns void. depth defaults to 0, visited defaults to a fresh WeakSet. | | temporalDateBackend | DateBackend ({ name: 'temporal'; codecs: Record<DateSchemaKind, DateCodec> }) | Nine codecs for @adaskothebeast/typewriter-runtime. name is the literal string 'temporal'. |

depth and visited exist so the function can recurse into itself; you normally pass only the first argument. Passing a non-zero depth lowers the remaining budget (the walker bails out once depth > 100), and passing a pre-populated visited set makes those objects be skipped.

temporalDateBackend codecs

Each codec is a DateCodec with is(value), parse(wire) and serialize(value); serialize is always value.toString().

| Schema kind | Runtime type | | ------------------ | --------------------------- | | instant | Temporal.Instant | | plain-date | Temporal.PlainDate | | plain-time | Temporal.PlainTime | | plain-date-time | Temporal.PlainDateTime | | zoned-date-time | Temporal.ZonedDateTime | | duration | Temporal.Duration | | period | Temporal.Duration | | plain-year-month | Temporal.PlainYearMonth | | plain-month-day | Temporal.PlainMonthDay |


⚡ Usage

import { hierarchicalConvertToTemporal } from '@adaskothebeast/hierarchical-convert-to-temporal';
import { Temporal } from '@js-temporal/polyfill';

const payload = {
  user: {
    name: 'Adam',
    createdAt: '2023-07-17T23:06:00.000Z',
    localReminder: '2023-07-17T23:06:00',
    sessions: [{ length: 'PT1H30M' }, { length: 'P0D' }],
  },
};

hierarchicalConvertToTemporal(payload);

payload.user.createdAt instanceof Temporal.Instant; // true
payload.user.localReminder instanceof Temporal.PlainDateTime; // true
payload.user.sessions[0].length instanceof Temporal.Duration; // true

As a fetch post-processing step:

async function getJson<T>(url: string): Promise<T> {
  const response = await fetch(url);
  const data = (await response.json()) as T;
  hierarchicalConvertToTemporal(data);
  return data;
}

Schema-driven hydration, where the shape decides the Temporal type instead of a regex:

import { temporalDateBackend } from '@adaskothebeast/hierarchical-convert-to-temporal';
import { createJsonTransformer } from '@adaskothebeast/typewriter-runtime';

import { userSchema } from './generated/user.schema';

const toUser = createJsonTransformer(userSchema, undefined, {
  dateBackend: temporalDateBackend,
});

const user = toUser(await response.json());

dateBackend is read by transformJson / serializeJson (and their create* wrappers), which look up backend.codecs[kind] for the date kind declared in the schema and throw Date backend "temporal" does not support <kind> when a kind is missing.


🎛️ Options and configuration

There are no options: no key allow-list, no format list, no target-type selection. Recognition is driven purely by the string content, in this order:

  1. Date-time gate. Length at least 19, - at index 4, - at index 7, T at index 10, then the regex ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|([+-]\d{2}:\d{2}))?$. One to nine fractional digits are allowed, so nanosecond precision survives.
  2. Instant or PlainDateTime. A trailing Z, or a +/- six characters from the end, means an exact time and yields Temporal.Instant.from(value). Otherwise Temporal.PlainDateTime.from(value).
  3. Duration gate. Length at least 3, starts with P or -P, then an ISO 8601 duration regex, yielding Temporal.Duration.from(value). The regex also accepts a comma as the decimal separator (PT0,5S).

Recursion is guarded by a depth budget of 100 and a WeakSet of already visited objects.


📤 Output examples

const input = {
  instant: '2023-07-17T23:06:00.000Z',
  offset: '2023-07-17T23:06:00.000+01:00',
  nanos: '2023-07-17T23:06:00.123456789Z',
  local: '2023-07-17T23:06:00',
  weeks: 'P4W',
  negative: '-PT1H',
  dateOnly: '2023-07-17',
  broken: '2023-99-99T99:99:99.000Z',
};

hierarchicalConvertToTemporal(input);
instant   -> Temporal.Instant       2023-07-17T23:06:00Z
offset    -> Temporal.Instant       2023-07-17T22:06:00Z      (offset folded into the exact time)
nanos     -> Temporal.Instant       2023-07-17T23:06:00.123456789Z
local     -> Temporal.PlainDateTime 2023-07-17T23:06:00
weeks     -> Temporal.Duration      P4W
negative  -> Temporal.Duration      -PT1H
dateOnly  -> '2023-07-17'           (unchanged, no time part)
broken    -> '2023-99-99T99:99:99.000Z' (unchanged, console.warn emitted)

Top-level arrays work too: ['P1Y2M4DT2H3M2S'] becomes [Temporal.Duration.from('P1Y2M4DT2H3M2S')].


⚠️ Edge cases

  • Prototype pollution is blocked. The keys __proto__, constructor and prototype are skipped entirely, so a hostile payload cannot reach Object.prototype. The side effect is that legitimate data parked under a key literally named constructor or prototype is never traversed or converted.
  • Inherited properties are ignored. Every key is checked with Object.hasOwn, so only own enumerable properties are visited.
  • Depth is capped at 100. Once depth > 100 the walker returns, leaving deeper strings as strings. This is DoS protection against adversarially nested JSON.
  • Circular graphs are safe. A WeakSet records visited objects, so input.self = input converts once and does not loop.
  • Offset information is lost for exact times. 2023-07-17T23:06:00.000+01:00 becomes a Temporal.Instant, which is UTC based. If you need the original offset or a time zone, use zoned-date-time through temporalDateBackend instead.
  • Offsetless date-times become PlainDateTime, which has no time zone at all. Comparing a PlainDateTime with an Instant throws, so a payload mixing both shapes needs explicit conversion (toZonedDateTime) before comparison.
  • Malformed but regex-matching strings stay strings. 2023-99-99T99:99:99.000Z passes the regex, Temporal.Instant.from throws, the error is caught and logged via console.warn('Failed to parse date string: ...'). Duration parse failures log Failed to convert duration string: ....
  • P alone is not a duration. The minimum length of 3 plus a lookahead requiring at least one digit rejects P, PT and -P.
  • False positives are possible. Recognition is content driven, never key driven, so an ISO-looking value in a note, label or id field is converted as well. The schema-driven @adaskothebeast/typewriter-runtime path exists exactly for payloads where that matters.
  • The polyfill is mandatory. Values are instanceof the polyfill's classes, not any native Temporal global. Mixing polyfill instances with a native Temporal implementation in the same process will fail instanceof checks.
  • Name collision with the runtime. @adaskothebeast/typewriter-runtime exports its own temporalDateBackend with the same nine codecs. Importing both into one module needs an alias (import { temporalDateBackend as temporalBackend } from ...). Pick one; they are interchangeable.
  • period and duration are the same codec pair. period exists so schemas generated from a java.time.Period or a .NET TimeSpan still resolve, but both map to Temporal.Duration.

🔗 Related packages

Full matrix and adapter recipes: main README.


📄 License

MIT © Adam Pluciński