Problem casting json propert to `Map<String,List<int>>`

why isn’t this possible in Dart?

import 'dart:convert';

final jsonString = '''{
    "calendar":  
    {
        "2024-11-18" : [601014]
    }
}
''';

void main() {
  final json = jsonDecode(jsonString) as Map<String, dynamic>;

  Map<String, List<int>> cal =
      (json['calendar']! as Map).cast<String, List<int>>();

  print(cal);
}

It throws

TypeError: Instance of 'JSArray<dynamic>': type 'List<dynamic>' is not a subtype of type 'List<int>

The underlying data should allow that.

1 Like

What works is this:

import 'dart:convert';

final jsonString = '''{
    "calendar":  
    {
        "2024-11-18" : [601014]
    }
}''';

void main() {
  final json = jsonDecode(jsonString) as Map<String, dynamic>;
  Map<String, List<int>> cal = (json['calendar'] as Map<String, dynamic>)
      .map((key, value) => MapEntry(key, (value as List).cast<int>()));
  print(cal);
}

but cast<> should be able to do this direclty

@munificent if you have a minute, I don’t understand why such a cast doesn’t work when the underlying data fits the types.

Time to start learning about patterns:

import 'dart:convert';

const jsonString = '''
{
    "calendar":  
    {
        "2024-11-18" : [601014]
    }
}
''';

void main() {
  final data = jsonDecode(jsonString);

  // extract the 2024-11-18
  if (data case {'calendar': {'2024-11-18': [final events]}}) {
    print('events: $events');
  }
}
1 Like

Are you sure that events then has the type List that I need?
also I can’t match on the the key of the nested map as that values can change on every API call.
My example is a simplified reproduction of what I to solve.
More over I would like to know why ‘cast’ doesn’t work here.

  1. Cast is not convert. Cast is “This is this base type, but I’m telling you that this is this derived type”. You can NEVER “cast” something to other type (i.e.: int to String is convert, not cast).

  2. Map without types is Map<dynamic>. You should NEVER use generics without type (or turn on strict-casts: true, strict-inference: true and strict-raw-types: true in analysis_options.dart to get warnings at least. Dart is pretty shitty regarding Generics. Then, again, cast is not convert, you can’t “cast” a Map<dynamic, dynamic> to Map<String, List<int>> because String does NOT inherit dynamic. In this case, you must to explicit convertion. Usually, jsonDecode returns a Map<String, dynamic> that can be used to, at least, get a reference to a value. But, then again, the result dynamic won’t be castable to List<int>, you’ll need to explicitly convert or, in Dart case, tell the compiler it is what you say it is (in this case, using the explicity cast operator as, which will result in a runtime exception when misused - it would be better to use patterns and verify for exceptional (hence, exception) cases.

2 Likes

The cast() method only casts one “level” deep into the collection. When you do:

Map<String, List<int>> cal =
      (json['calendar']! as Map).cast<String, List<int>>();

You’re expecting it to both coerce the map to Map<String, List<int> but also cast all of the elements to List<int>. But those inner casts (which are like as, not .cast()) fail because the reified type of the list is List<dynamic>, not List<int>.

It’s annoying but you have to manually coerce at every level if you have nested JSON like this.

5 Likes

Sorry to correct you but that cast() function of maps is exactly to tell Dart that you know that this dynamic value is in reality a map of a certain type.
Which it correctly does if you do it only on one level.

Casting a dynamic or Object to any type is perfectly legal if you know what the real type is

@munificent but why? For my understanding the cast () method just function says to treat the map as if it where of the other type which will lead to run time errors if it isn’t.
I wound understand if I would try to cast it so some custom class but we are talking here about lists and simple types.
I C++ this would be like a reinterpret_cast.
Especially for generated code this makes dealing with deserialization why more difficult.

Granted, this is an interesting discussion and I may not have encountered a cast like this yet, but I think the idea of ​​the reinterpret_cast in C++ is helpful in understanding this problem.
In C++ for example you can’t do:

int x = 0;
double y = reinterpret_cast<double>(x);

as in this discussion you can’t cast from dynamic to List<int>.

reinterpret_cast is used with pointers/references and the above code is not a reinterpretation. For example the correct way is to use something like:

double y = *reinterpret_cast<double*>(&x);

and within Dart I think is not possible to do it this way.

1 Like

agreed it’s not identical to a reinterpret_cast but I don’t see why this couldn’t work that way

Very interesting observation

I don’t think this error is related to the nested nature of the json though. Because the following code which isn’t nested also fails with the same error, and I think the source is the same.

final List<dynamic> list = [123];
final arr = list as List<int>;
print(arr);

type ‘List’ is not a subtype of type ‘List’ in type cast

In your code when I catch the exception it fails at

 void forEach(void f(K key, V value)) {
    _source.forEach((SK key, SV value) {
      f(key as K, value as V);
    });
  }

Here the value as V is an identical cast of List<dynamic> to List<int> as in my example

Hope someone can shine some light on why this as fails though.


What’s even weirder is that as doesnt work, but cast does work fine

  final List<dynamic> list = [123];
  // final arr = list as List<int>;
  final arr = List.castFrom<dynamic, int>(list);
  print(arr);

But based on any doc I could find, both are type cast in dart, so why the difference

1 Like

@Hari_07 can you try your example with List.cast<T> ?

Yeah that works too

final List<dynamic> list = [123];
  // final arr = list as List<int>;
  final arr = list.cast<int>();
  print(arr);

Internally they’re the same

List<R> cast<R>() => List.castFrom<E, R>(this);

No, that’s not what it does. If that’s all you wanted, you’d use as Map<String, List<int>>.

In reality, the map is a Map<String, dynamic>. It is not a Map<String, List<int>>:

import 'dart:convert';

final jsonString = '''{
    "calendar":
    {
        "2024-11-18" : [601014]
    }
}
''';

void main() {
  final json = jsonDecode(jsonString) as Map<String, dynamic>;
  final calendar = json['calendar']!;
  print(calendar is Map<String, dynamic>); // Prints "true".
  print(calendar is Map<String, List<int>>); // Prints "false".
}

Likewise, the lists inside there are List<dynamic> they are not List<int>:

import 'dart:convert';

final jsonString = '''{
    "calendar":
    {
        "2024-11-18" : [601014]
    }
}
''';

void main() {
  final json = jsonDecode(jsonString) as Map<String, dynamic>;
  final calendar = json['calendar']!;
  final list = calendar['2024-11-18']!;

  list.add('definitely not an int!');
  print(list); // Prints "[601014, definitely not an int!]".
}

What cast() does is produce a new map with the desired key and value types. Then, when you access elements, it lazily casts (using as) each entry’s key and value to the expected types. That’s exactly and all that it does.

It doesn’t do any deep conversion. I don’t think it would be possible to implement a “deep cast” version of cast() that recursively calls cast() on the keys and values if they are types that support that. As far as I can tell, Dart doesn’t expose enough runtime type information (or compile-time specialization) to support that.

2 Likes