Why there is not loadData() method in FileSystem? To use the private:// scheme of Drupal 8 I had to :

Create a folder outside the docroot folder
Implement a  $settings['file_private_path'] = '../private-files'; to designate this folder
Make sure authorization matches the website user to write into it

Then I load the filesystem helper (I could use injection dependency but that is not the point here) :
$this-&gt;filesystem = \Drupal::service('file_system');

Prepare the directory in case it does not exists :
$this-&gt;filesystem-&gt;prepareDirectory('private://any_folder', FileSystemInterface::CREATE_DIRECTORY);

And then I could save a (unmanaged) file:
$this-&gt;filesystem-&gt;saveData(json_encode(['My Beautiful File Body']), 'private://any_folder/any_file.json', FileSystemInterface::EXISTS_REPLACE);

And this works fine.
But to load the file, I would have expected to have an &quot;opposite&quot; (say, loadData() ) method in $this-&gt;filesystem.
And as the file is unmanaged (which is fine) I cannout use File Entity::load() to load it.
So I end up with :
$loadedFile = json_decode(file_get_contents('private://any_folder/any_file.json'), JSON_OBJECT_AS_ARRAY);

Which works fine, but raises questions :

Is this a legitimate way to do it (meaning, using a Drupal helper to save a file, then loading it from a straight PHP function) ?

Why there is no loadData() method in the FileSystem service as I would have expected ?

How does the Drupal scheme 'public:// or private://) is resolved from this PHP function ?
To answer your questions in respective order:

Sure. If there's no built-in API function to make it easier, there's nothing wrong with using file_get_contents since the function can use Drupal's file stream protocols.
The core test,
FileSaveDataTest::testFileSaveData(), is doing this to confirm that saveData() works.
One can only guess about the requirements as the class was composed. saveData() is a wrapper around a file_put_contents() op so that logging can take place when Drupal writes to a temp file. Perhaps there was no need to wrap file_get_contents in the same way?
Drupal has a StreamWrapperManager service class that registered different custom myscheme:// streams with PHP. This manager makes use of core PHP function stream_wrapper_register() to register any injected class that has the tags: { name: stream_wrapper, scheme: myscheme }. For example, look at CoreServiceProvider::register() which registers the private:// scheme when it detects file_private_path settings has been set.