1

While implementing a simple credential store based on JSON files I ran into a problem where I need to do a seemingly unnecessary roundtrip back to JSON for Dart's oauth2.Credentials.fromJson() constructor to work.

Background: I am following the oauth2 package's Authorization Code Grant example but with the difference that instead of a single Credentials object, I want to save a list of Credentials.

Here is a stripped-down version of my credential store:

import 'dart:convert';
import 'dart:io';
import 'package:oauth2/oauth2.dart';

class CredentialStore {
  List<Credentials> get credentials {
    // credentials.json would contain a JSON array of objects created with oauth2.Credentials.toJson()
    final file = File('credentials.json');
    final jsonString = file.readAsStringSync();
    final List cred = jsonDecode(jsonString);
    return List<Credentials>.from(cred.map((e) => Credentials.fromJson(e)));
  }
}

The last line is adapted from this answer and is accepted by the compiler but fails like this on runtime:

Unhandled exception:
type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'String'

The code, however, runs successfully, if I take a round-trip back to JSON when mapping, like so:

return List<Credentials>.from(cred.map((e) => Credentials.fromJson(jsonEncode(e))));

This seems unnecessary and looks bad to me. Is this essentially a problem with the Credentials.fromJson() implementation in that it cannot accept maps already parsed from JSON or is it possible to rewrite my get credentials implementation in a way that avoids encoding back to JSON?

basse
  • 1,088
  • 1
  • 19
  • 40

1 Answers1

1

The Credentials class exposes the Credentials() constructor, which is what fromJson also uses.

If you take a look at the implementation of the fromJson factory, it does some basic validation on the JSON object (i.e. checks that it's valid JSON and the required fields are present) and passes the parsed data to Credentials().

Since your data comes from Credentials.toJson() that you control yourself, it's probably safe to forgo the validation and use the constructor directly:

import 'dart:convert';
import 'dart:io';
import 'package:oauth2/oauth2.dart';

class CredentialStore {
  List<Credentials> get credentials {
    final file = File('credentials.json');
    final jsonString = file.readAsStringSync();
    final List cred = jsonDecode(jsonString);
    
    return List<Credentials>.from(cred.map((parsed) {
      var tokenEndpoint = Uri.parse(parsed['tokenEndpoint']);
      var expiration = DateTime.fromMillisecondsSinceEpoch(parsed['expiration']);
      return Credentials(
        parsed['accessToken'],
        refreshToken: parsed['refreshToken'],
        idToken: parsed['idToken'],
        tokenEndpoint: tokenEndpoint,
        scopes: (parsed['scopes'] as List).map((scope) => scope as String),
        expiration: expiration
      );
    }));
  }
}

Regarding your compiler error: in the answer you've linked, the fromJson method there operates on the result of a JSON decoded Map, whereas the Credentials.fromJson() factory operates on a String and as such does not expect the value passed to it to be already decoded.

cbr
  • 12,563
  • 3
  • 38
  • 63
  • Thank you for a very thorough answer! One more question: I was trying to use the constructor with the spread operator (`...`). Am I correct that it cannot be used in constructor calls or would it be possible to do something like `Credentials(parsed['accessToken'], ...rest)` to avoid writing out all of the field names? I suppose that would require some kind of casting for the types to work if at all possible? – basse Jul 24 '22 at 16:11
  • 1
    I'm not familiar enough with Dart to know. [This answer](https://stackoverflow.com/a/56279163/996081) from 2019 says no, but that may have changed during the past 3 years. – cbr Jul 24 '22 at 18:25