Skip to content

Schema.DateFromString and Schema.Date emit no format: "date-time" in JSON Schema #6790

Description

@alandotcom

What version of Effect is running?

effect@4.0.0-beta.102

What steps can reproduce the bug?

import { Schema } from "effect"

const schema = Schema.Struct({
  createdAt: Schema.DateFromString,
  updatedAt: Schema.Date,
  name: Schema.String,
})

const standard = Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))

console.log(
  JSON.stringify(
    standard["~standard"].jsonSchema.input({ target: "draft-2020-12" }),
    null,
    2
  )
)

What is the expected behavior?

createdAt should carry format: "date-time", since its encoded side is an ISO 8601 string:

{
  "createdAt": { "type": "string", "format": "date-time" },
  "name": { "type": "string" }
}

format: "date-time" is JSON Schema's own vocabulary for this, and the other Standard Schema libraries emit it.

Here is zod@4.4.3 on the equivalent shape, for comparison:

z.toJSONSchema(
  z.object({
    createdAt: z.iso.datetime(),
    updatedAt: z.date(),
    name: z.string(),
  }),
  { unrepresentable: "any" }
)
{
  "createdAt": { "type": "string", "format": "date-time", "pattern": "..." },
  "updatedAt": {},
  "name": { "type": "string" }
}

arktype emits the keyword too, through its fallback.date option.

What do you see instead?

All three properties render identically, with nothing marking either date:

{
  "createdAt": { "type": "string" },
  "updatedAt": { "type": "string" },
  "name": { "type": "string" }
}

Passing libraryOptions makes no difference.

Note the two distinct problems there. For Schema.DateFromString the description is incomplete: the value really is a string, and the keyword saying which kind of string is missing. For Schema.Date the description is wrong: it is a Date-instance-only schema in v4 (#6481), so a Date is what both sides hold, and "type": "string" describes a value the schema would reject. Zod renders that same case as {}, declining to describe what has no JSON form.

Additional information

This matters for consumers reading a schema through the Standard Schema interface, where the JSON Schema is the entire description available. ~standard.validate returns a decoded value and ~standard.jsonSchema describes the shape, and neither reports that a property is a date. The information exists in the AST (the Declaration node carries annotations.representation.id === "effect/schema/Date"), but a library-agnostic consumer cannot reach it without special-casing Effect.

The practical effect is that a tool accepting any Standard Schema has to ask Effect users to hand-annotate format: "date-time" on a string and pipe it to a date, reimplementing Schema.DateFromString, while Zod and arktype users write their library's normal date spelling and it just works.

Related, both closed and neither covering this: #3662 (JSONSchema.make threw on Schema.Date in v3) and #6481 (v4 migration guide gap for Schema.Date).

Suggested fix

Both paths route through one internal schema, so this looks like a one-line change. Schema.ts:11814:

const DateString = String.annotate({ expected: "a string that will be decoded as a Date" })

DateFromString pipes it (Schema.ts:11921) and Date's toCodecJson links to it (Schema.ts:11856), so annotating it there covers both:

const DateString = String.annotate({
  expected: "a string that will be decoded as a Date",
  format: "date-time"
})

There is already a precedent for exactly this, in the same file at Schema.ts:13242:

const Base64String = String.annotate({
  expected: "a base64 encoded string that will be decoded as Uint8Array",
  format: "byte",
  contentEncoding: "base64"
})

Schema.Uint8Array consequently emits { "type": "string", "format": "byte", "contentEncoding": "base64" }, which is the shape being asked for here. isUUID does the same thing through a check annotation (Schema.ts:6928).

I confirmed the annotation survives both routes:

const AnnotatedString = Schema.String.annotate({ expected: "x", format: "date-time" })

AnnotatedString
// { "type": "string", "format": "date-time" }

AnnotatedString.pipe(Schema.decodeTo(Schema.Date, SchemaTransformation.dateFromString))
// { "type": "string", "format": "date-time" }

Possibly the same gap nearby

Two neighbouring schemas render as bare strings and have a standard keyword available:

  • Schema.URL emits { "type": "string" }; JSON Schema has format: "uri".
  • Schema.DateTimeUtc emits { "type": "string" }; format: "date-time" applies.

Happy to send a PR for any or all of these if the direction is right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions