# Stremio Addon Serialization: String vs String-Array

Added: 2026-06-02 session.

## The Problem

Many Stremio addons return single strings for fields that the Stremio protocol spec declares as JSON string arrays:

```json
{
  "meta": {
    "name": "The Shawshank Redemption",
    "director": "Frank Darabont",        // ← STRING, should be ["Frank Darabont"]
    "cast": "Tim Robbins, Morgan Freeman", // ← STRING, should be ["Tim Robbins", "Morgan Freeman"]
    "genres": "Drama"                      // ← STRING, should be ["Drama"]
  }
}
```

Kotlinx Serialization crashes:
```
kotlinx.serialization.json.internal.JsonDecodingException:
Expected start of the array '[', but had '"' instead at path: $.meta.director
```

## Affected Fields in MetaItem

All four List<String> fields in `MetaItem` were vulnerable:
- `genres: List<String>?`
- `cast: List<String>?`
- `director: List<String>?`
- `writer: List<String>?`

## The Fix — StringOrListSerializer

Applied in `Models.kt`:

```kotlin
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonPrimitive

object StringOrListSerializer : KSerializer<List<String>> {
    override val descriptor: SerialDescriptor =
        PrimitiveSerialDescriptor("StringOrList", PrimitiveKind.STRING)

    override fun deserialize(decoder: Decoder): List<String> {
        val jsonDecoder = decoder as? JsonDecoder ?: return emptyList()
        val element = jsonDecoder.decodeJsonElement()
        return when (element) {
            is JsonArray -> element.map { it.jsonPrimitive.content }
            is JsonPrimitive -> {
                val content = element.content
                if (content.isNotEmpty()) listOf(content) else emptyList()
            }
            else -> emptyList()
        }
    }

    override fun serialize(encoder: Encoder, value: List<String>) {
        // Read-only deserialization from addon APIs
    }
}

// Apply to MetaItem fields:
@Serializable(with = StringOrListSerializer::class)
val director: List<String>? = null,
@Serializable(with = StringOrListSerializer::class)
val cast: List<String>? = null,
@Serializable(with = StringOrListSerializer::class)
val writer: List<String>? = null,
@Serializable(with = StringOrListSerializer::class)
val genres: List<String>? = null,
```

## Key Design Decisions

1. **Use `is JsonArray` / `is JsonPrimitive`, not extension properties.** The `element.jsonArray` extension property throws `JsonException` if the element is not a `JsonArray`. Same for `element.jsonPrimitive`.

2. **Kotlinx handles nullability automatically.** The field type is `List<String>?` — the framework wraps `StringOrListSerializer` with a nullable serializer. The custom `deserialize()` only fires for non-null JSON values.

3. **Return `emptyList()` for unknown types.** If the JSON is somehow neither array nor primitive (e.g., an object), return empty list to avoid crash. Better to show nothing than crash the app.

4. **Serialize is a no-op.** We only read from Stremio addons, never write back.

## Pattern Recognition

**Diagnostic signature:** `Expected start of the array '[', but had '"'` + `path $.meta.<field_name>`. Any new `@Serializable` data class field typed `List<String>?` that receives Stremio addon JSON should use this serializer.

## Testing

The serializer handles these cases:
```kotlin
"director": "Frank Darabont"              → ["Frank Darabont"]
"director": ["Frank Darabont"]            → ["Frank Darabont"]
"director": ["Frank Darabont", "Author"]  → ["Frank Darabont", "Author"]
"director": ""                            → []
"director": null                          → null (handled by nullable wrapper)
"director": {}                            → [] (unknown type fallback)
```
