What is FSET
FSET is a shallow tree structure editor that produces target data, with simple modular composability
FSET's products:
- FModel : A schema editor that helps system owners model and author JSON Schema using a nice subset for data modeling
FSET is a shallow tree structure editor that produces target data, with simple modular composability
FSET's products:
FModel's visual is inspired by Algebraic Data Type to represent type definition with block based structural constraints.
A fmodel example:
type VConcatSpec<GenericSpec> = { }
bounds : ||
| "full"
| "flush"
center : bool
data : ||
| D :: Data
| null
description : string
name : string
resolve : R :: Resolve
spacing : float_64
title : ||
| T :: Text
| T :: TitleParams
transform : [ T :: Transform ]
vconcat : [ S :: Spec ]
A fmodel encodes a json schema definition, source: https://github.com/vega/schema
FModel itself has no syntax (it can be displayed however html is capable of, though FModel displays it like text based because of familiarity and cleanliness; text and symbols are already great). So, there is no schema language introduced here, and no syntax errors (one would argue that a little bit of allowed mistakes enables more productivity). It's like editting high level AST where each node is a semantic block. A block represents a target data. However, it's easier to model stuff when we think in terms of type. In the end fmodels get exported as definitions along with a root schema starting from an entrypoint. FModel provides ease of constructing schema, constraints that prevent errors, automated features and documentation.
FModel has a logical group called "modu", stands for module (naming is not a big deal it's just short enough for good noise/signal ratio visually). A module contains a list of fmodels (i.e. top level types). That's it. Not as much as a "module system". Module name is used with fmodel name at export. Currently, a list of module is flat, there is no folder for now. It's enough for majority of schema today (however, the editor will have a way to group module in future, maybe something like scoped labels, because module and ref belong to FSET that will also have other kind of editors)
A Ref type can refer to a fmodel across modules. It also provides a simple referential integrity enforced at database level (i.e. Postgresql's foreign key constraint), if a fmodel is being referenced, it's not removable. Ref name automatically reflects referenced fmodel name and namespace when type is updated or moved between modules.
FModel currently support 1 entrypoint at a time when export as a single file schema. A fmodel can be marked as entrypoint, and it will be exported to root level of JSON Schema of its own module, the rest of fmodels will go under "$defs" keyword (currently, without tree shaking; unused defs elimination) each with module name namespace.
FModel uses a stable subset of draft-2020-12; keywords that have been through from several previous releases to latest release. It would probably have some new keywords in future draft if that's useful in data modeling. Ideally, we do not ever remove keywords especially ones that strengthen constraints (i.e. fail a validation that's previously passed)
A record can “be extended by another record” by default, optionally can be marked as strict. Strict record means to be at top level of composition while lax record means to be reusable definitions. The trade-off of lax record in exchange for composibility is that it loses mistyped field name feedback to users if it’s used as a standalone.
{
"properties": {
"field_a": {
"type": "string"
},
"field_b": {
"maximum": 2147483647,
"minimum": -2147483648,
"type": "integer"
}
},
"required": [
"field_a"
],
"type": "object"
}
Simply called E Recrod, is a record that extends another record (including e_record). It’s currently possible that two records may have duplicate field (by labels), there’s no static analysis on this in our editor. During validation process, if there exists duplicate field, their assertion result is equivalent to intersection of the types for that particular field.
{
"$ref": "https://localhost/ExportDocs#/$defs/Record",
"properties": {
"field_c": {
"type": "boolean"
}
},
"type": "object"
}
Strictless record without specified fields means it validates any property successfully.
{
"properties": {},
"type": "object"
}
In this case record, properties are strictly empty. No other record can extend this, and the only object data that will pass this schema validation is {}, literally an empty object.
{
"properties": {},
"type": "object",
"unevaluatedProperties": false
}
List element is homogeneous; all elements are the same type.
Options: (uniqueItems: bool)
{
"items": {
"$ref": "https://localhost/ExportDocs#/$defs/Record"
},
"type": "array"
}
Tuple is positional, fixed length, always strict. That means there is no thing like extensible tuple.
{
"maxItems": 3,
"minItems": 3,
"prefixItems": [
{
"const": "ok"
},
{
"$ref": "https://localhost/ExportDocs#/$defs/Record"
},
{
"properties": {},
"type": "object"
}
],
"type": "array"
}
Since output here is json, dict key is always a string type. FModel editor also enforces the rule. String type with its options is put under propertyNames
Options: (minProperties: integer, maxProperties: integer)
{
"additionalProperties": {
"$ref": "https://localhost/ExportDocs#/$defs/Record"
},
"minProperties": 4,
"propertyNames": {
"pattern": "^x-",
"type": "string"
},
"type": "object"
}
tagged_union (1)
Tagged Union only allow elements to be record or ref-to-record type. With configurable tag name, the tag name is a property merged into every tagged element.
This type is also known as “Sum type”, it is for high certainty in data modeling, so we make it always strict. Even if each tagged element is lax record, when composed into this type, extra fields will fail a validation.
{
"oneOf": [
{
"properties": {
"_mytag": {
"const": "tag_a"
},
"field_x": {
"maximum": 255,
"minimum": 0,
"type": "integer"
}
},
"required": [
"_mytag"
],
"type": "object"
},
{
"properties": {
"_mytag": {
"const": "tag_b"
},
"field_z": {
"type": "string"
}
},
"required": [
"_mytag"
],
"type": "object"
}
],
"unevaluatedProperties": false
}
tagged_union (2)
Tagged Union element whose type is ref-to-record, tag is left unmerged.
{
"oneOf": [
{
"$ref": "https://localhost/ExportDocs#/$defs/LaxRecord",
"properties": {
"_mytag": {
"const": "tag_a"
}
},
"required": [
"_mytag"
]
},
{
"properties": {
"_mytag": {
"const": "tag_b"
},
"field_z": {
"type": "string"
}
},
"required": [
"_mytag"
],
"type": "object"
}
],
"unevaluatedProperties": false
}
Unlike tagged union, untagged union is NOT usually used for switch-case -like reasoning (pattern match and deconstruct constructed data), but more like a bag of values.
The example intentionally shows how untagged union can introduce confusion easily. Overusing this with a bunch of complicated constrants will make it hard to think “what’s the look of possible values?” at current level.
We recommend only use untagged union with scalar types or scalar values.
{
"anyOf": [
{
"$ref": "https://localhost/ExportDocs#/$defs/Record"
},
{
"$ref": "https://localhost/ExportDocs#/$defs/Dict"
},
{
"type": "string"
},
{
"type": "null"
}
]
}
When all union elements are scalar values, the union is automatically exported as enum.
Options: (asUnion: bool) fallback to a general union that can have, e.g. description
{
"enum": [
"a",
10,
true,
null
]
}
Options: (minLenth: integer, maxLenth: integer, pattern: string, default: string, format: string)
Currently, our editor only validates default against minLength and maxLength. But there will definitely be other validation.
{
"maxLength": 10,
"minLength": 5,
"type": "string"
}
Options: (default: bool)
{
"default": true,
"type": "boolean"
}
{
"type": "null"
}
Integer which allows explicit range specified. Options: (multipleOf: integer, default: integer, format: string)
All integer preset range below also have the same options except (minimum: number, maximum: number)
{
"default": 100,
"maximum": 500,
"minimum": 100,
"multipleOf": 5,
"type": "integer"
}
8-bit signed integer. Fixed range. For any explicit range, choose integer type.
{
"default": 50,
"maximum": 127,
"minimum": -128,
"type": "integer"
}
16-bit signed integer. Fixed range. For any explicit range, choose integer type.
{
"maximum": 32767,
"minimum": -32768,
"type": "integer"
}
32-bit signed integer. Fixed range. For any explicit range, choose integer type.
{
"maximum": 2147483647,
"minimum": -2147483648,
"type": "integer"
}
8-bit unsigned integer. Fixed range. For any explicit range, choose integer type.
{
"maximum": 255,
"minimum": 0,
"type": "integer"
}
Currently, no difference between float32 and float64 on output. However, there is format option.
Options: (format: string)
{
"type": "number"
}
Options: (format: string)
{
"default": 3.1111111,
"type": "number"
}
{
"$ref": "https://localhost/ExportDocs#/$defs/Record"
}
{
"const": "a"
}
{
"const": false
}
{
"const": null
}
16-bit unsigned integer. Fixed range. For any explicit range, choose integer type.
{
"maximum": 65535,
"minimum": 0,
"type": "integer"
}
32-bit unsigned integer. Fixed range. For any explicit range, choose integer type.
{
"maximum": 4294967295,
"minimum": 0,
"type": "integer"
}
This is a value of float64. In JSON, it’s the number type. The only limitation is that its interger range on our editor is int53
{
"const": 1.1
}
{}
Purpose of FModel Import is for "getting started" quickly from existing schema. Imported schema is NOT going to be lossless. We expect `draft 2020-12`, but our chosen keywords are likely what you are already using; those stable ones.
Output from FModel to JSON Schema section is expected to be input. If source schema file's `$defs` or `definitions` name has namespace, import will try to group by that namespace, for example, "AWS_ACMPCA_Policy". "AWS_ACMPCA" is going to be module name, "Policy" is going to be one of module's fmodels. By default, imported definitions are grouped by first character.