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
FModel
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.
Why not use JSON Schema keywords directly?
JSON Schema is a set of constraints based keywords, not only it contains "if", "then, "else", but also its keyword independence nature.
That means a cartesian product of all keywords is enormous, like you can freely compose a bunch of logic gates encoding business rules at schema level.
It's expressive, but you will be inclined to put logics that should rather be at application level in there. That does not fit data modeling mental model (something that is not a model of data may look like configuration or parameters)
FModel picks a nice subset of it and provides visual representation (and block constraints) that guides our brain to think in terms of model and type over logical constraints. In our observation, majority of JSON Schema users have used it this way all along,
but there is still a lot of confusion, especially for newcomers, trying to mix and match to form rules that are not sane.
Goals
While FModel helps system developers author schema in "data modeling" way, it still uses standard JSON Schema
Vocabulary without customization or our own vocabulary.
So it's still in
"Validation" category, not in
"Code Generation" category because validation is what JSON Schema is designed for.
So today, FModel only exports JSON Schema original vocabulary (based on
draft/2020-12,
"Each release of the JSON schema specification is treated as a production release").
FModel output schema is suitable for configuration validation, api request / respond body, and also data shape like
data visualization grammar.
Generally speaking, FModel exports standard, widely adopted, data modeling schema in json format.
There is
JSON Type Definition (rfc8927) which is designed for
code generation, if there is enough
demand, FModel could also export JTD, though we would have yet another standard because code generation under JSON Schema umbrella, they just
started developing it and FModel will export that as well when some releases come out.
How about non JSON and/or binary format export?
No Plan!
Module and Ref
Module
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)
Ref
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.
Entrypoint
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.
References
Export
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"
}
-
{
"default": true,
"type": "boolean"
}
-
-
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"
}
-
-
-
-
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
}
-
Import
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.
Keyboard commands
Add
- Add random schema to container types
- Shift + +
Move
- Cut selected schemas
- Cmd + x
- Copy selected schemas
- Cmd + c
- Paste
- Cmd + v
- Cancel copy or cut
- Escape
Clone
- Clone selected schemas
-
Shift + Alt + ArrowUp
Shift + Alt + ArrowDown
Reorder
- Reorder selected schemas up or down
-
Alt + ArrowUp
Alt + ArrowDown
Collapse / Expand
- Collapse selected schemas
- ArrowLeft
- Expand selected schemas
- ArrowRight
Delete
- Delete selected schemas
- Delete
Select
- Select a schema
-
ArrowUp
ArrowDown
- Select a sibling schema
-
i
j
- Select mutiple schemas
-
Shift + ArrowUp
Shift + ArrowDown
- Select mutiple schema all the way
-
Shift + Cmd + ArrowUp
Shift + Cmd + ArrowDown
- Select the first child
- Cmd + ArrowUp
- Select the last child
- Cmd + ArrowDown
- Select the root element of tree
- Home
- Select the last element of tree
- End
Rename key
- Enable key editing on a selected schema
- Enter
- Submit a changed key with current text input value
- Enter
- Cancel editing
- Escape
Change type
- Enable type editing on a selected schema
- Shift + Enter
- Submit a changed type with current text input value
- Enter
- Cancel editing
- Escape
- For Windows, use Ctrl in place of Cmd