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

✨ Proposal: Support $ref-to-enum in object required #1551

Open
etinquis opened this issue Oct 26, 2024 · 5 comments
Open

✨ Proposal: Support $ref-to-enum in object required #1551

etinquis opened this issue Oct 26, 2024 · 5 comments
Labels
proposal Initial discussion of a new idea. A project will be created once a proposal document is created.

Comments

@etinquis
Copy link

etinquis commented Oct 26, 2024

Describe the inspiration for your proposal

High-Level goal:
I want to be able to consolidate and re-use what are practically-speaking a static set of property names or object keys within a schema such that I am able to maintain the list in one place and reference them as either object property names or values, depending on context.

[My examples will assume I'm trying to define a schema for a localization data structure]

I start with a few enum schemas containing my keys.

{
    "$id": "/translation-slug.json",
    "title": "TranslationSlug",
    "enum": ["my_string_1", "my_string_2", /*...*/ ]
}
{
    "$id": "/locale.json",
    "title": "Locale",
    "enum": ["en", "fr", /*...*/ ]
}

I can reference this enum as values for properties on objects as so

{
    "$id": "/translation-assignment.json",
    "title": "TranslationAssignment",
    "type": "object",
    "properties": {
       "locale": { $ref: "/locale.json" },
       "slug": { $ref: "/translation-slug.json" },
       "translator": { $ref: '/translator.json' }
    },
   "required": ["locale", "slug", "translator"]
}
{
   "locale": "en",
   "slug": "my_string_2",
   "translator": "me"
}

I can reference this enum to define property names on objects as so

{
    "$id": "/translated-batch.json",
    "title": "TranslatedBatch",
    "type": "object",
    "propertyNames": {
       "$ref": "/locale.json"
    },
    "additionalProperties": {
       "type": "object",
       "propertyNames": {
          "$ref": "/translation-slug.json"
       },
       "additionalProperties": {
         "type": "string"
       }
    }
}
{
   "en": {
     "my_string_1": "..."
   },
   "de": {
     "my_string_2": "..."
   }
}

Now I want to define my 'source-of-truth' translated string file format schema, where I want validation to fail if any property is missing:

{
  "en": {
      "my_string_1": "...",
      "my_string_2": "...",
      "my_string_3": "..."
   },
   "fr": {
      /* my_string_1 is missing! */
      "my_string_2": "...",
      "my_string_3": "..."
    },
   /* de is missing! */
}

Describe the proposal

Possibilities:

An 'overload' on required directly allowing references to enum schemas

{
    "$id": "/translations.json",
    "title": "Translations",
    "type": "object",
    "propertyNames": {
       "$ref": "/locale.json"
    },
    "additionalProperties": {
       "type": "object",
       "propertyNames": {
          "$ref": "/translation-slug.json"
       },
       "additionalProperties": {
         "type": "string"
       },
       "required": { "$ref": "/translation-slug.json" }
    },
    "required": { "$ref": "/locales.json" }
}

A new required-adjacent property allowing references to enum schemas

{
    "$id": "/translations.json",
    "title": "Translations",
    "type": "object",
    "propertyNames": {
       "$ref": "/locale.json"
    },
    "additionalProperties": {
       "type": "object",
       "propertyNames": {
          "$ref": "/translation-slug.json"
       },
       "additionalProperties": {
         "type": "string"
       },
       "requiredPropertiesEnum": { "$ref": "/translation-slug.json" }
    },
    "requiredPropertiesEnum": { "$ref": "/locales.json" }
}

Some way to define propertyNames from an enum in a way that also makes them all required

{
    "$id": "/translations.json",
    "title": "Translations",
    "type": "object",
    "propertyNamesRequired": {
       "$ref": "/locale.json"
    },
    "additionalProperties": {
       "type": "object",
       "propertyNamesRequired": {
          "$ref": "/translation-slug.json"
       },
       "additionalProperties": {
         "type": "string"
       }
    }
}

Describe alternatives you've considered

The only way I understand there to be to implement this currently to duplicate and inline the values separately into 'required'.

{
    "$id": "/translations.json",
    "title": "Translations",
    "type": "object",
    "propertyNames": {
       "$ref": "/locale.json"
    },
    "additionalProperties": {
       "type": "object",
       "propertyNames": {
          "$ref": "/translation-slug.json"
       },
       "additionalProperties": {
         "type": "string"
       },
       "required": [ "my_string_1", "my_string_2", ... ]
    },
    "required": [ "en", "fr", ... ]
}

This imposes a potentially significant maintenance burden whereby changes to the enum must be duplicated in the subset of places where the full set should be required. Failure to propagate those changes also risks falsely validating schemas 'silently' or failing what should be valid schemas without some sort of linting procedure that is capable of identifying this pattern.

Additional context

https://github.com/orgs/json-schema-org/discussions/818 was my original Q&A discussion post looking for whether this was possible.

@etinquis etinquis added the proposal Initial discussion of a new idea. A project will be created once a proposal document is created. label Oct 26, 2024
@etinquis
Copy link
Author

None of the possibilities above consider any form of composition of multiple enums, which may also be a nice feature if it's possible.

@etinquis
Copy link
Author

etinquis commented Oct 26, 2024

An potential alternate proposal:

Adjust the semantics of required such that it behaves similarly to propertyNames.

That is, required is a string-validating schema that evaluates against the properties present on an object, and where 'valid' resolves to required and 'invalid' resolves to not-required.

The current list definition is substituted with an enum schema with the inline values, which might provide backwards-compatibility?

EDIT: Or maybe that doesn't make sense. 🤔
It'd have to intersect with the properties defined, and somehow still be able to identify missing keys.

@etinquis
Copy link
Author

etinquis commented Oct 27, 2024

Alternative alternative proposal:

required is a string array-validating schema over all present property names, with the current list case being constructed via contains.

I assume this wouldn't surface errors very clearly.

EDIT: e.g.

{
   "required": ["property1", "property2"]
}

potentially equivalent to

{
  "required": {
    "type": "array",
    "allOf": [
      {
        "contains": {
          "const": "property1"
        }
      },
      {
        "contains": {
          "const": "property2"
        }
      }
    ],
    "additionalItems": true,
    "uniqueItems": true
  }
}

and I believe the enum case could be

{
   "required": {
     "type": "array",
     "items": {
          "$ref": "/enum.json"
     },
     "additionalItems": false,
     "uniqueItems": true
   }
}

EDIT: this enum example is not sufficient. It passes if there is a single enum value. I still need a way to assert that all enum values are present, which I'm having trouble thinking of a construction for.

@jdesrosiers
Copy link
Member

I'm sympathetic to the problem you're trying to solve. It's definitely a real problem that I'd love to have a solution for. Unfortunately, I don't see any of these suggestions fitting into the JSON Schema architecture. Schemas are always evaluated against a JSON instance and they produce a true/false (valid/invalid) result and some annotations. All of your proposals require some kind alternate evaluation of a schema to extract a value from the schema. That's not something JSON Schema is designed to do and I don't think that's a good path to go down.

Instead of trying to solve the specific problem of using the same array for enum and required, I think it might make more sense to try to address the generic problem that each of these proposals is really doing underneath. The problem is that you can't reference values that aren't schemas. In this case you need to reference an array.

{
  "properties": {
    "a": { "enum": { "$ref": "#/x-locales" } },
    "b": {
      "type": "object",
      "additionalProperties": { "type": "boolean" },
      "required": { "$ref": "#/x-locales" }
    }
  },
  "x-locales": ["en", "fr", ...]
}

But, there are other use-cases as well.

{
  "minLength": 5,
  "maxLength": { "$ref": "#/minLength" }
}

For those to work, it would require that we change $ref to allow referencing non-schemas and that we revert $ref to it's previous semantics (replaces the whole reference object with it's result vs a keyword in a schema object). I don't think that's going to get a lot of support, so I'd try to think of a way to get the same kind of behavior in another way.

@etinquis
Copy link
Author

All of your proposals require some kind alternate evaluation of a schema to extract a value from the schema. That's not something JSON Schema is designed to do and I don't think that's a good path to go down.

Agreed 100% with respect to the possibilities listed in the OP. Even as the ignorant consumer that I am, they certainly feel like unidiomatic hackery.

I think #1551 (comment) is the best I've got so far down the required road.

Instead of trying to solve the specific problem of using the same array for enum and required, I think it might make more sense to try to address the generic problem that each of these proposals is really doing underneath. The problem is that you can't reference values that aren't schemas. In this case you need to reference an array.

Agreed here too, insofar as I think the ability to externalize and reference the array itself would help to solve the issue.

Some additional thoughts, though, that lead me to feel that the required-as-schema may still be a worthwhile pursuit:

To my mind, I want to define enums as sets (granted, you could consider it specifically the array part of the enum schema as the set). To validate a single value against the enum schema is to ask is {value} a member of the {enum} set. Sometimes, as in the case of trying to come up with a schema that would fit 'required' , I want to be able to ask is {array} equal to the {enum} set (or subset/superset of), and (as far as I've found so far) it seemed like the closest I could get was to 'explode' the individual values into contains: { allOf: [ { const: "one" }, { const: "two" }, { ... } ] } or similar. That doesn't feel right, and there must be a better way.

I don't have a complete thought on this yet, but I wonder if there's room for applying set operations within array schemas against enums-as-a-set that would support crafting schemas that could both conform to requirements for 'required', as well as unlock the potential for set composition/intersection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
proposal Initial discussion of a new idea. A project will be created once a proposal document is created.
Projects
None yet
Development

No branches or pull requests

2 participants