{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"field-schema@1","title":"Okonomi generator field schema, version 1","description":"What a generator contract's fields look like. This document is a JSON Schema *describing* the field format; the field format itself is not JSON Schema, because JSON Schema cannot carry a translated label, a unit, a group, a scope, a widget or an enum value's name without putting every one of them in a vendor extension. JSON Schema was the wrong tool for being that format and is the right tool for validating one: a second vendor's contract can be checked by anything off the shelf.","type":"object","required":["fieldSchema","groups","inputs"],"properties":{"fieldSchema":{"const":"field-schema@1","description":"Which version of this format the document speaks. First key in the body, so a reader who opened a file called schema.json knows immediately that it is not one."},"groups":{"type":"array","minItems":1,"description":"The headings the fields are rendered under. At least one: a heading that carries a translation cannot be implicit, so there is no ungrouped case to render around.","items":{"$ref":"#/$defs/group"}},"inputs":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/field"}}},"$defs":{"i18n":{"type":"object","description":"A human-readable string in every language the publisher has it in. `default` is required and is English; any other key is a locale tag, and a consumer with no entry for its locale falls back to `default`.","required":["default"],"properties":{"default":{"type":"string","minLength":1}},"additionalProperties":{"type":"string","minLength":1}},"key":{"type":"string","pattern":"^[a-z][a-z0-9_]*$","description":"snake_case, and unique across the whole document rather than within a group: a field is named the same wherever it is rendered."},"group":{"type":"object","required":["id","order","label"],"properties":{"id":{"$ref":"#/$defs/key"},"order":{"type":"integer","minimum":0},"label":{"$ref":"#/$defs/i18n"}},"additionalProperties":false},"choice":{"type":"object","description":"One value of an enum, what it is called and what it looks like. The value stays an English code because it is stored data and goes into a cache key. `label` *names* the value and is translated; `color` and `thumbnail` *show* it and are not — a colour is a colour in every language, and a translated URL is a URL that 404s in German. Both are optional and both are absent from an ordinary enum: there is no colour for DIN links. They are carried where the value names a material, because a select holding eight lacquers is unreadable as eight lines of text and a second fetch of the material catalogue is a lot of ceremony for a swatch.","required":["value","label"],"properties":{"value":{"type":"string","minLength":1},"label":{"$ref":"#/$defs/i18n"},"color":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$","description":"sRGB, six digits and nothing else — the same rule a material's own colour is held to, so a client never has to parse two colour formats. Where the material has maps this is the mean of its diffuse map, which is what makes it the right fallback rather than an approximation."},"thumbnail":{"type":"string","pattern":"^https?://","description":"An absolute URL to a picture of this value. A pattern rather than `format: \"uri\"` because this document is validated in ajv's strict mode without `ajv-formats`, where an unknown format is refused outright rather than quietly ignored — and because what actually matters here is the one thing the pattern says: it is absolute. A relative one cannot be repaired downstream. Absent where there is nothing to picture: a lacquer is a colour and `color` is the whole of it. Absolute and resolved per request, never stored, because a URL built from one host outlives that host."}},"additionalProperties":false},"field":{"type":"object","required":["key","scope","role","group","dataType","label","default","widget"],"properties":{"key":{"$ref":"#/$defs/key"},"label":{"$ref":"#/$defs/i18n"},"scope":{"enum":["type","generator"],"description":"Who mandated this field. `type` is the product type's contract, the same for every vendor; `generator` is this vendor's own."},"role":{"enum":["dimension","material","configuration"],"description":"What the field is *for*, which is the axis a client groups and styles on. `dataType` says a value is an enum and `group` says it is rendered under a heading; neither says whether the control picks a size or a finish, and a group holds both — a Blendrahmen's depth and its finish are one heading. Before this the only thing separating them was a `_material` suffix on the key, which is a convention no second vendor is bound by. `dimension` is a measurement, `material` picks a finish for a slot, `configuration` is everything else. A count is deliberately not a dimension: the number of glazing bars decides geometry but it is not a size."},"group":{"$ref":"#/$defs/key"},"dataType":{"enum":["integer","decimal","boolean","enum","string","list","object"]},"unit":{"type":"string","minLength":1},"options":{"$ref":"#/$defs/optionList"},"min":{"type":"number"},"max":{"type":"number"},"limitMin":{"type":"number"},"limitMax":{"type":"number"},"default":{"description":"What an unstated key resolves to, in the shape the field describes: a scalar for a scalar, an array for a list, an object for an object. Never null — an input without a default is an input a caller cannot omit, and every field here can be omitted.","type":["number","string","boolean","array","object"]},"widget":{"enum":["slider","number","switch","select","segmented","path"]},"step":{"type":"number","exclusiveMinimum":0},"visibleIf":{"type":"object","minProperties":1},"items":{"description":"What each element of a list is: a field descriptor without the parts that belong to a named input. Required when dataType is \"list\", absent otherwise.","$ref":"#/$defs/element"},"fields":{"description":"The named parts of an object. Required when dataType is \"object\", absent otherwise.","type":"array","minItems":1,"items":{"$ref":"#/$defs/field"}},"minItems":{"description":"How short a list may be.","type":"integer","minimum":0},"maxItems":{"description":"How long a list may be.","type":"integer","minimum":1}},"additionalProperties":false,"allOf":[{"if":{"properties":{"dataType":{"const":"enum"}},"required":["dataType"]},"then":{"properties":{"options":{"$ref":"#/$defs/optionList"}},"required":["options"]},"else":{"not":{"required":["options"]}}},{"if":{"properties":{"dataType":{"const":"list"}},"required":["dataType"]},"then":{"required":["items"],"properties":{"items":{"$ref":"#/$defs/element"}}}},{"if":{"properties":{"dataType":{"const":"object"}},"required":["dataType"]},"then":{"required":["fields"],"properties":{"fields":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/field"}}}}}]},"optionList":{"type":"array","minItems":1,"items":{"$ref":"#/$defs/choice"}},"element":{"description":"One element of a list. It has no key, no group and no label, because it is not something a form lists — everything that constrains a value is still here.","type":"object","additionalProperties":false,"required":["dataType"],"properties":{"dataType":{"enum":["integer","decimal","boolean","enum","string","list","object"]},"unit":{"type":"string","minLength":1},"options":{"$ref":"#/$defs/optionList"},"min":{"type":"number"},"max":{"type":"number"},"limitMin":{"type":"number"},"limitMax":{"type":"number"},"default":{"description":"What an unstated key resolves to. A scalar for a scalar field; an array for a list, an object for an object — the same shape the field describes."},"step":{"type":"number","exclusiveMinimum":0},"visibleIf":{"type":"object","minProperties":1},"items":{"description":"What each element of a list is: a field descriptor without the parts that belong to a named input. Required when dataType is \"list\", absent otherwise.","$ref":"#/$defs/element"},"fields":{"description":"The named parts of an object. Required when dataType is \"object\", absent otherwise.","type":"array","minItems":1,"items":{"$ref":"#/$defs/field"}},"minItems":{"description":"How short a list may be.","type":"integer","minimum":0},"maxItems":{"description":"How long a list may be.","type":"integer","minimum":1}}}}}