这是indexloc提供的服务,不要输入任何密码
Skip to content

Use patternProperties when applicable #5449

@rickosborne

Description

@rickosborne

I have a model where text values can include optional localization via key suffixes:

{
    // The default value for whatever the primary language is
    label: "Purple",
    // Localization via a ISO language code suffix on the key
    "label:en": "Purple",
    "label:ru": "Пурпурный",
    "label:zh": "紫色"
}

In terms of types, I end up with something like:

type WithLocalizedLabel = {
    label: string;
   [`label:${string}`]: string;
}

In JSON Schema, I'd use:

{
    patternProperties: {
        "^label:[a-z]{2}$": { type: "string" },
    },
    properties: {
        label: { type: "string" }
    },
    type: "object"
}

For a zod validator, I use:

const zLabeled = z.object({ label: z.string() });
const zLocalizedLabeled = z.record(z.regex(/^label:[a-z]{2}$/), z.string());
const zWithLocalizedLabel = zLabeled.and(zLocalizedLabeled);

That code generates the expected TypeScript types. The problem is with the generated JSON Schema. You get output like:

{
    $defs: {
        Labeled: {
            properties: {
                label: { type: "string" }
            },
            type: "object"
        },
        LocalizedLabeled: {
            additionalProperties: {
                type: "string"
            },
            propertyNames: {
                pattern: "^label:[a-z]{2}$"
                type: "string"
            },
            type: "object"
        },
        WithLocalizedLabel: {
            allOf: [
                { $ref: "#/$defs/Labeled" },
                { $ref: "#/$defs/LocalizedLabeled" },
            ]
        }
    }
}

The problem there is, of course, that allOf doesn't really mean quite the same as &. It just merges the two schemas, instead. So what you end up with is effectively:

{
    properties: {
        label: { type: "string" }
    },
    additionalProperties: {
        type: "string"
    },
    propertyNames: {
        pattern: "^label:[a-z]{2}$"
        type: "string"
   },
    type: "object"
}

This causes problems as label does not match the pattern.

Instead, what I'd actually like to end up with is for the record JSON Schema to look like:

{
    properties: {
        label: { type: "string" }
    },
    patternProperties: {
        "^label:[a-z]{2}$": { type: "string" }
    },
    type: "object"
}

I know one workaround would be to modify the pattern to make the suffix optional. But an option to trigger the use of patternProperties instead would also allow me to use strict i.e. additionalProperties: false, which would be nice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions