-
Notifications
You must be signed in to change notification settings - Fork 230
Description
As requested by @opqdonut
I've noticed that Malli's JSONSchema transformation behaviour differs from Zod's, which causes problems with LLM structured output APIs (at the moment I don't think any of them support oneOf so they just return 'nil'). Zod outputs an anyOf with the discriminator key required, whereas Malli outputs a oneOf (also with the discriminator required). Originally I was wondering if there was a way to change the behaviour by specifying an argument to (json-schema/transform) but I'm currently working around this by annotating the :multi type with a :json-schema/type attribute and closing the schema before requesting structured output.
Outputs below.
Zod:
const TimePrecision = z.enum([
"decade",
"year",
"quarter",
"month",
"week",
"day",
"hour",
"minute",
"second",
]);
const TimeBase = z.object({
precision: TimePrecision,
display: z.string(),
});
const AbsoluteTime = TimeBase.extend({
temporalType: z.literal("absolute"),
timestamp: z.iso.datetime(),
}).describe("Absolute temporal reference e.g. 'on Saturday'");
const RelativeTime = TimeBase.extend({
temporalType: z.literal("relative"),
anchor: z.iso.datetime(),
offset: z.iso.duration(),
}).describe("Relative temporal reference e.g. 'last week'");
const RangeTime = TimeBase.extend({
temporalType: z.literal("range"),
earliest: z.iso.datetime(),
latest: z.iso.datetime(),
}).describe("Range time reference e.g. 'some time between 1971 and 1975'");
export const TemporalReferenceStrict = z
.discriminatedUnion("temporalType", [AbsoluteTime, RelativeTime, RangeTime])
.meta({
name: "temporal-reference",
description:
"A temporal reference - should only be encoded to the precision that the patient actually describes",
});> z.toJSONSchema(TemporalReferenceStrict)
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
name: "temporal-reference",
description: "A temporal reference",
anyOf: [
{
description: "Absolute temporal reference e.g. 'on Saturday'",
type: "object",
properties: {
precision: { type: "string", enum: [Array] },
display: { type: "string" },
temporalType: { type: "string", const: "absolute" },
timestamp: {
type: "string",
format: "date-time",
pattern: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
}
},
required: [ "precision", "display", "temporalType", "timestamp" ],
additionalProperties: false
},
{
description: "Relative temporal reference e.g. 'last week'",
type: "object",
properties: {
precision: { type: "string", enum: [Array] },
display: { type: "string" },
temporalType: { type: "string", const: "relative" },
anchor: {
type: "string",
format: "date-time",
pattern: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
},
offset: {
type: "string",
format: "duration",
pattern: "^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$"
}
},
required: [ "precision", "display", "temporalType", "anchor", "offset" ],
additionalProperties: false
},
{
description: "Range time reference e.g. 'some time between 1971 and 1975'",
type: "object",
properties: {
precision: { type: "string", enum: [Array] },
display: { type: "string" },
temporalType: { type: "string", const: "range" },
earliest: {
type: "string",
format: "date-time",
pattern: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
},
latest: {
type: "string",
format: "date-time",
pattern: "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
}
},
required: [ "precision", "display", "temporalType", "earliest", "latest" ],
additionalProperties: false
}
]
}
Malli:
(def precision
[:enum {:description "Granularity of the temporal reference"}
:decade :year :quarter :month :week :day :hour :minute :second :millisecond])
(def ^:private time-base
[:map
[:precision precision]
[:display [:string {:min 1 :description "Original free-form temporal description (e.g. \"Saturday 12 May 2025\", \"last week\")"}]]])
(def reference
[:multi {:dispatch :temporal-type
:description "Temporal reference encoded as absolute, range, or relative representation"}
[:absolute
[:merge time-base
[:map
[:temporal-type [:enum :absolute]]
[:timestamp :iso-partial]]]]
[:relative
[:merge time-base
[:map
[:temporal-type [:enum :relative]]
[:anchor {:description "Anchor of the temporal reference - either a datetime or a named event"}
[:or :iso-partial :uuid-v7]]
[:offset :iso-duration]]]]
[:range
[:map
[:temporal-type [:enum :range]]
[:display [:string {:min 1 :description "Original free-form temporal description (e.g. \"Saturday 12 May 2025\", \"last week\")"}]]
[:earliest :iso-partial]
[:earliest-precision precision]
[:latest :iso-partial]
[:latest-precision precision]]]])(json-schema/transform reference)
{:description "Temporal reference encoded as absolute, range, or relative representation",
:oneOf
[{:type "object",
:properties
{:precision
{:description "Granularity of the temporal reference",
:type "string",
:enum [:decade :year :quarter :month :week :day :hour :minute :second :millisecond]},
:display
{:description "Original free-form temporal description (e.g. \"Saturday 12 May 2025\", \"last week\")",
:type "string",
:minLength 1},
:temporal-type {:type "string", :enum [:absolute]},
:timestamp {:type "string"}},
:required [:precision :display :temporal-type :timestamp]}
{:type "object",
:properties
{:precision
{:description "Granularity of the temporal reference",
:type "string",
:enum [:decade :year :quarter :month :week :day :hour :minute :second :millisecond]},
:display
{:description "Original free-form temporal description (e.g. \"Saturday 12 May 2025\", \"last week\")",
:type "string",
:minLength 1},
:temporal-type {:type "string", :enum [:relative]},
:anchor
{:description "Anchor of the temporal reference - either a datetime or a named event",
:anyOf [{:type "string"} {:type "string", :format "uuid"}]},
:offset {:anyOf [{:$ref "#/definitions/time.duration"} {:$ref "#/definitions/time.period"}]}},
:required [:precision :display :temporal-type :anchor :offset]}
{:type "object",
:properties
{:temporal-type {:type "string", :enum [:range]},
:display
{:description "Original free-form temporal description (e.g. \"Saturday 12 May 2025\", \"last week\")",
:type "string",
:minLength 1},
:earliest {:type "string"},
:earliest-precision
{:description "Granularity of the temporal reference",
:type "string",
:enum [:decade :year :quarter :month :week :day :hour :minute :second :millisecond]},
:latest {:type "string"},
:latest-precision
{:description "Granularity of the temporal reference",
:type "string",
:enum [:decade :year :quarter :month :week :day :hour :minute :second :millisecond]}},
:required [:temporal-type :display :earliest :earliest-precision :latest :latest-precision]}],
:definitions {"time.duration" {:type "string", :format "duration"}, "time.period" {}}}Metadata
Metadata
Assignees
Labels
Type
Projects
Status