# Reviving polymorphic JSON

The [built-in metadata](https://github.com/javiercejudo/modelico/tree/c356c97466264d859bea3d0ad8f9542f3bfd0894/docs/introduction/metadata.md) covers the bulk of use cases. However, to deal with types whose JSON might take more than one form, you will need a custom reviving strategy.

We are going to walk through two examples: first, we will revive objects based on an enumerated field that will indicate how the object should be revived; second, we will revive objects based on their shape only, without any additional fields.

To deal with fields where several subclasses of the declared type would need to be revived, see the ["runtime type" recipe](https://modelico.javiercejudo.com/recipes/runtime_type_for_subclasses).

## Example 1: revive based on an enumerated field

We are going to create a `NumberCollection` class which models either an `M.StringMap` or an `M.List` to parse JSON like this:

```javascript
/* eg. 1: */ {"collectionType": "OBJECT", "collection": {"a": 1, "b": 2}}
/* eg. 2: */ {"collectionType": "ARRAY" , "collection": [1, 2, 3, 4, 3]}
```

*Note: the convention is to name the type field as simply `type`, but we will continue with `collectionType` to show that it is up to you.*

First, we can use `M.Enum` to create `CollectionType`:

```javascript
const CollectionType = M.Enum.fromArray(['OBJECT', 'ARRAY'])
```

Now, we can create our `NumberCollection` class:

```javascript
const {_, number, stringMap, list, anyOf} = M.metadata()

class NumberCollection extends M.Base {
  getNumbers () {
    const {collectionType, collection} = this

    switch (collectionType()) {
      case CollectionType.OBJECT():
        // Note that .inner() creates a copy. For improved performance,
        // [...collection()[M.symbols.innerOrigSymbol]().values()] is used
        return [...collection()[M.symbols.innerOrigSymbol]().values()]
      case CollectionType.ARRAY():
        return [...collection()]
      default:
        throw TypeError(`Unsupported NumberCollection with type ${collectionType.toJSON()}`)
    }
  }

  sum () {
    return this.getNumbers().reduce((acc, x) => acc + x, 0)
  }

  static innerTypes () {
    return Object.freeze({
      collectionType: _(CollectionType),
      collection: anyOf([
        [stringMap(number()), CollectionType.OBJECT()],
        [list(number()), CollectionType.ARRAY()]
      ], 'collectionType') // if omitted, the enumerated field is 'type'
    })
  }
}
```

Note that the value for each field in `innerTypes` can be either metadata or a metadata-returning function that will be passed the plain object being revived.

We can now use it as follows:

```javascript
const col1 = M.fromJSON(NumberCollection, `
  {
    "collectionType": "OBJECT",
    "collection": {"a": 10, "b": 25, "c": 4000}
  }
`)

col1.sum() // => 4035
```

```javascript
const col2 = M.fromJSON(NumberCollection, `
  {
    "collectionType": "ARRAY",
    "collection": [1, 2, 3, 4, 3]
  }
`)

col2.sum() // => 13
```

In this case, the serialisation side of things will work out of the box, since `M.List`, `M.StringMap` and our `CollectionType` implemented with an `M.Enum`, implement `.toJSON()` methods on their instances:

```javascript
JSON.stringify(col1)
// => {"collectionType":"StringMap","collection":{"a":10,"b":25,"c":4000}}

JSON.stringify(col2)
// => {"collectionType":"List","collection":[1,2,3,4,3]}
```

## Example 2: revive based on the shape of the value

First, it is worth mentioning this is not always possible, as the shape of the JSON representation might be ambiguous (see example in [Gson's RuntimeTypeAdapterFactory](https://github.com/google/gson/blob/gson-parent-2.8.0/extras/src/main/java/com/google/gson/typeadapters/RuntimeTypeAdapterFactory.java#L36)).

In this example, we are going to revive the same polymorphic JSON as the one above, but without an enumerated field to hint the type of the collection.

```javascript
const {number, stringMap, list} = M.metadata()

class NumberCollection extends M.Base {
  getNumbers () {
    const collection = this.collection()

    return (collection[M.symbols.typeSymbol]() === M.List)
      ? [...collection]
      : [...collection[M.symbols.innerOrigSymbol]().values()]
  }

  sum () {
    return this.getNumbers().reduce((acc, x) => acc + x, 0)
  }

  static innerTypes () {
    return Object.freeze({
      collection: v => Array.isArray(v.collection)
        ? list(number())
        : stringMap(number())
    })
  }
}
```

The end result is simpler, but less generic. It might require non-trivial updates to the logic that figures out which metadata to use. For example, if we start supporting `map(number())`, whose JSON representation is an array of pairs, `Array.isArray` will not be enough.
