Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Certain schemas to define a record's keys are discarded #124

Open
Spappz opened this issue Jun 17, 2024 · 1 comment
Open

Certain schemas to define a record's keys are discarded #124

Spappz opened this issue Jun 17, 2024 · 1 comment
Labels
bug Something isn't working

Comments

@Spappz
Copy link
Contributor

Spappz commented Jun 17, 2024

Back again!

JSON obviously doesn't support records, so they're represented as objects with restrictions (where possible) on the property names and values. The package's handling is pretty good, but it seems to give up in two cases I've found. In both cases, the package converts the schemas correctly outside a record key, so I'm unsure why that same schema can't be simply bundled into JSON Schema's propertyNames property.

I unfortunately haven't managed to work out which part of the code actually causes this behaviour, though. I presume something about parsers/record.ts, since the schemas are converted properly in other environments, but I didn't see anything pop out as odd!

Minimal reproducible examples

Examples below using CJS require() so you can test in the REPL. I've used the same schema in both the record's keys and values to highlight that the issue only occurs with the former.

Composed schemas

The inclusion of a union or intersection causes the key schema to be discarded.

const { z } = require("zod");
const { zodToJsonSchema } = require("zod-to-json-schema");

const schema = z.string().url().or(z.string().email());
const zodSchema = z.record(schema, schema);

const jsonSchema = zodToJsonSchema(zodSchema);
console.log(JSON.stringify(jsonSchema, null, "   "));
// Output
{
   "type": "object",
   "additionalProperties": {
      "anyOf": [
         {
            "type": "string",
            "format": "uri"
         },
         {
            "type": "string",
            "format": "email"
         }
      ]
   },
   "$schema": "http://json-schema.org/draft-07/schema#"
}
// Expected
{
   "type": "object",
   "additionalProperties": {
      "anyOf": [
         {
            "type": "string",
            "format": "uri"
         },
         {
            "type": "string",
            "format": "email"
         }
      ]
   },
   "propertyNames": {
      "anyOf": [
         {
            "type": "string",
            "format": "email"
         },
         {
            "type": "string",
            "format": "uri"
         }
      ]
   },
   "$schema": "http://json-schema.org/draft-07/schema#"
}

z.literal()

z.literal() causes that schema to be discarded, rather than being correctly converted to JSON Schema's const.

The example below is fairly pathological, since the actual object being described here is just { foo?: "foo" }. The case in which I actually encountered the bug, before reducing it down, was the slightly more reasonable

z.record(externalStringSchema.or(z.literal("specialCase")), valueSchema)

This is a composed schema as above, but I determined that any amount of z.literal() causes a problem. So, although I do think the below case can fall under a wontfix/user error, I think it's probably still undesired due to the higher-order ramifications.

const { z } = require("zod");
const { zodToJsonSchema } = require("zod-to-json-schema");

const schema = z.literal("foo");
const zodSchema = z.record(schema, schema);

const jsonSchema = zodToJsonSchema(zodSchema);
console.log(JSON.stringify(jsonSchema, null, "   "));
// Output
{
   "type": "object",
   "additionalProperties": {
      "type": "string",
      "const": "foo"
   },
   "$schema": "http://json-schema.org/draft-07/schema#"
}
// Expected
{
   "type": "object",
   "additionalProperties": {
      "type": "string",
      "const": "foo"
   },
   "propertyNames": {
      "type": "string",
      "const": "foo"
   },
   "$schema": "http://json-schema.org/draft-07/schema#"
}
@StefanTerdell StefanTerdell added the bug Something isn't working label Jun 18, 2024
@StefanTerdell
Copy link
Owner

StefanTerdell commented Jun 18, 2024

Back again!

🥳


Here's the block checking for propertyNames compatibility:

if (
def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodString &&
def.keyType._def.checks?.length
) {
const keyType: JsonSchema7RecordPropertyNamesType = Object.entries(
parseStringDef(def.keyType._def, refs),
).reduce(
(acc, [key, value]) => (key === "type" ? acc : { ...acc, [key]: value }),
{},
);
return {
...schema,
propertyNames: keyType,
};
} else if (def.keyType?._def.typeName === ZodFirstPartyTypeKind.ZodEnum) {
return {
...schema,
propertyNames: {
enum: def.keyType._def.values,
},
};
}

Adding your requirements here should do the trick. It could also use a general clean up, but the idea of ignoring any output that isn't a string is still sound as it follows spec (from what I can remember anyway).

"type": "string" is implicit in the root, that's why it's (clumsily) removed. "anyOf"'s with "type" should be fine though as per your example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants