app logo

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 to JSON Schema (draft 2020-12)

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
    }
                        
  • {}
                        

Import

JSON Schema (draft 2020-12) to FModel

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