Skip to content

Instantly share code, notes, and snippets.

@alcatrazEscapee
Last active July 9, 2020 22:34
Show Gist options
  • Save alcatrazEscapee/287847eccf5fef862cb082f7981d8e15 to your computer and use it in GitHub Desktop.
Save alcatrazEscapee/287847eccf5fef862cb082f7981d8e15 to your computer and use it in GitHub Desktop.
JSON Mixin

JSON Mixin Specification

Overview

This is intended to be a specification for allowing multiple JSON files to be merged at parse time into a single file. It's intended application is in Minecraft modding, for allowing multiple mods to make modifications to the same JSON file without causing conflicts. This has been a problem mainly caused by block loot tables (and was eventually solved by global loot tables), but with the introduction of all of world generation in 1.16.2 snapshots, this method may not prove useful in the long run.

The goal is to make a data driven JSON merging system, which satisfies the needs of mods to inject, replace, or modify existing JSONs. In Minecraft, this could be accomplished by a top level tag, e.g. "__apply__": "mixin" to designate a specific JSON as a mixin and load it appropriately. Additionally, it could be possible to have another tag (global_mixin) which causes it to apply to every possible file of the same type (much as in the way that global loot functions work).

Requirements:

  • Replace specific values within a json object
  • Add or append specific values to arrays, or key value pairs to objects
  • Do the above, but applied globally to all possible json files
  • Conditionally apply modifiers based on the presence of specific json elements

1. Primitive Values

Any primitive value (boolean, string, number, etc) when present in a mixin, will either inject the value in the target JSON if it does not exist, or replace the value if it does. In addition, if the value is null, then the value will be removed.

1.1 Example: Adding a Primitive Value

With the original JSON:

{}

With the following Mixin applied:

{
  "value": true
}

Will result in the following result:

{
  "value": true
}

1.2 Example: Replacing a Primitive Value

With the original JSON:

{
  "value": true
}

With the following Mixin applied:

{
  "value": false
}

Will result in the following result:

{
  "value": false
}

1.3 Example: Removing a Primitive Value

With the original JSON:

{
  "value": true
}

With the following Mixin applied:

{
  "value": null
}

Will result in the following result:

{}

2. JSON Objects

A JSON Object, when present in a mixin, will do several things, based on the state of the target JSON:

  • If the key is not present in the target JSON, a new key will be created with a value of {}
  • If the key is present, but the value is not an object, the value will be replaced with {}
  • If the key is present, and the value is an object, nothing will happen

Then, the contents of the object will be applied as a mixin, to the newly created object.

There is one special case: if the mixin object contains NO keys, then it is a signal that the target object should also be replaced with an object containing no keys.

2.1 Example: Appending to a JSON Object

With the original JSON:

{
  "nested": {
    "old_value": false
  }
}

In order to add properties of the nested object, the structure must be recreated within the mixin:

{
  "nested": {
    "new_value": true
  }
}

This will result in the following JSON

{
  "nested": {
    "old_value": false,
    "new_value": true
  }
}

2.2 Example: Removing from a JSON Object

With the original JSON:

{
  "nested": {
    "old_value": false,
    "new_value": true
  }
}

In order to remove a value, the structure must be recreated, and the value to be removed must be inserted with a null value:

{
  "nested": {
    "old_value": null
  }
}

This will result in the following JSON:

{
  "nested": {
    "new_value": true
  }
}

2.3 Example: Removing a JSON Object

With the original JSON:

{
  "nested": {
    "old_value": false,
    "new_value": true
  }
}

If the desired result is to remove the nested object entirely, it's value can simply be specified as null within the mixin:

{
  "nested": null
}

Which will result in the following JSON:

{}

2.4 Example: Removing all keys from a JSON Object

With the original JSON:

{
  "nested": {
    "old_value": false,
    "new_value": true
  }
}

If the desired result is to remove the all keys from the nested object, but leave the object, or remove all keys without specifying them individually, it can be specified as an empty object:

{
  "nested": {}
}

Which will result in the following JSON:

{
  "nested": {}
}

3. JSON Arrays

In order to insert, replace, and inject into existing JSON arrays, a way to specify an index into an array is necessary. In order to do this, a special JSON object notation is required for fine grained control within JSON arrays. However, if this discreteness is not required, then JSON arrays can still be used directly with limited effectiveness.

When using JSON Arrays directly in mixins, the array elements are merged, with the mixin elements appended to the existing values.

3.1 Example: Appending to a JSON Array (Using a JSON Array)

With the following original JSON:

{
  "list": ["cat", "dog", "bear"]
}

The following mixin can be used to append values:

{
  "list": ["snake", "badger"]
}

Which will result in the following json:

{
  "list": ["cat", "dog", "bear", "snake", "badger"]
}

3.2 Modifying JSON Arrays using __apply__

In order to modify specific elements of an array, or insert elements at specific positions, in place of the array, an object should be placed with the special tag "__apply__": "array". Elements of the array can be accessed for mixins by using the key of the numeric index into the array. For example, in the mixin:

{
  "list": {
    "__apply__": "array",
    "0": true
  }
}

The element at "0" indicates that this should be mixed into the first element of the array. Similarly, any non-negative integer will replace a specific index within the array.

These elements are treated as mixins themselves. So in order to modify an element within an array, the element can be specified as a JSON object and the standard mixin rules would apply.

Finally, there are two special keyword indexes: begin and end. These are used to inject values at the beginning and end of the array, respectively. They must be a list of elements to be inserted. Thus, the following mixin using JSON array notation:

{
  "list": ["snake"]
}

Can also be implemented using the object notation for arrays:

{
  "list": {
    "__apply__": "array",
    "end": ["snake", "badger"]
}

3.3 Example: Inserting at the Front of a JSON Array

With the following original JSON:

{
  "list": ["cat", "dog", "bear"]
}

The following mixin can be used to append values:

{
  "list": {
    "__apply__": "array",
    "begin": ["snake", "badger"]
}

Which will result in the following json:

{
  "list": ["snake", "badger", "cat", "dog", "bear"]
}

3.4 Example: Replacing Elements of a JSON Array

With the following original JSON:

{
  "list": ["cat", "dog", "bear"]
}

The following mixin can be used to replace specific values:

{
  "list": {
    "__apply__": "array",
    "0": "snake",
    "2": "badger"
}

Which will result in the following json:

{
  "list": ["snake", "dog", "badger"]
}

3.5 Example: Modifying Elements of a JSON Array

With the following original JSON:

{
  "list": [{
    "type": "cat",
    "name": "Mr Fluffers"
  }, {
    "type": "dog",
    "name": "Ms Woolf"
  }]
}

The following mixin can be used to modify an element at a specific index:

{
  "list": {
    "__apply__": "array",
    "0": {
      "name": "Mr Fluffers Jr."
    }
  }
}

Which will result in the following json:

{
  "list": [{
    "type": "cat",
    "name": "Mr Fluffers Jr."
  }, {
    "type": "dog",
    "name": "Ms Woolf"
  }]
}

4. Conditional Applications

It is often desirable to be able to have mixins which will only apply under a specific circumstance, for instance the existence of a particular path or value within JSON. This format has some support for both of those situations.

In order to specify a condition, a top level item must be present in the mixin:

{
  "__if__": {}
}

The contents of the __if__ object are considered the conditions, and the object itself matches the structure of the main JSON body.

For every property declared in this condition object, two things are necessary:

  • If the property's key exists on the target JSON, and
  • If the property has a non null value, then the value must match exactly the target JSON.

4.1 Example: Conditional on a Path

The following mixin:

{
  "__if__": {
    "path": null
  }
}

Would require the existence of a top level property named path, with an arbitrary value. Thus it would apply to the following target JSON:

{
  "path": true
}

But it would not apply to this JSON:

{}

4.2 Example: Conditional on a Nested Path

In order to condition on nested paths, multiple objects are required. The following mixin:

{
  "__if__": {
    "nested": {
      "path": null
    }
  }
}

Requires the existence of the paths nested, and path under that. Thus it would apply to the following target JSON:

{
  "nested": {
    "path": true
  }
}

But it would not apply to this:

{
  "nested": true,
  "path": false
}

Or this:

{}

4.3 Example: Conditional on a Value

In order to condition on a value, the value, including the path, must be specified as part of the JSON. The following mixin requires the existence of a path three, with the value 3

{
  "__if__": {
    "three": 3
  }
}

This would apply to the following target JSON:

{
  "three": 3
}

But not the following JSON:

{
  "three": 4
}

4.4 Conditioning on Array Indexes

In order to condition on array indexes directly, a very similar array object notation is used as before. JSON Arrays can be directly inserted, in which case they will match the indexes in ascending order. Or, the "__apply__": "array" tag can be used to match specific numeric indexes.

The only difference from the previous use of the array object notation is that the begin and end keywords are unused. The only valid keys besides the __apply__ tag are numeric indexes.

4.5 Example: Conditioning on Array Indexes

The following JSON uses an array to verify that the first three indexes exist.

{
  "__if__": {
    "path": [null, null, null]
  }
}

It would apply to the following target JSON:

{
  "path": [0, 1, 2, 3]
}

But not the following:

{
  "path": [0, 1]
}

4.6 Example: Conditioning on Array Indexes with Objects

The above example can also be rewritten using the object array notation:

{
  "__if__": {
    "path": {
      "__apply__": "array",
      "0": null,
      "1": null,
      "2": null
    }
  }
}

This will produce the exact same condition as above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment