Enhanced return types - automatic type gen based on content

Created on 14 February 2024, 4 months ago
Updated 7 May 2024, about 2 months ago

Problem/Motivation

Some discussion here: https://git.drupalcode.org/project/api_client/-/merge_requests/47#note_2...

Inspired by TypeScript libraries like Prisma, tRPC, many GraphQL clients, and more, it would be great to have the json returned from the clients to be strongly typed.

Currently we use generics to cast our return types to the passed in generic, but this falls flat for a few reasons, one among them is the inability for the methods to know if they are returning the RawResponse or just the Json. This might still be a problem we'll have to solve with some fancy conditional types or otherwise, but I believe it is solvable.

Steps to reproduce

Proposed resolution

Might need to do some more research to understand how to map the schema in Drupal to TypeScript types.

This repo has a list of TypeScript libraries, many of them have some sort of type generation:
https://github.com/jellydn/awesome-typesafe

This will probably involve some sort of mechanism to scrape the backend into a cache (to speed future requests) and transform that cache to TS types. https://github.com/mike-north/jsonapi-typescript might get us a good bit of the way there, and I would hope we could use and contribute to that project if we need to instead of starting from scratch.

Remaining tasks

Non exhaustive list of TODOS:
- Research the proper approach for a schema based type generation
- Gather feedback
- Implement and document
- Gather feedback

User interface changes

API changes

Data model changes

✨ Feature request
Status

Active

Component

Code

Created by

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Comments & Activities

  • Issue created by @coby.sher
  • πŸ‡ΊπŸ‡ΈUnited States brianperry
  • πŸ‡΅πŸ‡±Poland Marcin Maruszewski

    Is there a roadmap for this issue? I ask because I've been thinking for a while about creating some sort of example of Drupal's ability to use JSON:API and Typescript in NextJS, but I don't quite know how to combine it.

    I've been looking at jsonapi_schema and json-schema-to-typescript, but I can see that at this stage these are not things that would make working with types easier. They generate weird field names, weird types and don't give you the ability to link entities to each other. Or at least I don't know how to do that.

    I'll be watching with interest to see further progress on this topic.

  • Status changed to Active 3 months ago
  • πŸ‡ΊπŸ‡ΈUnited States brianperry

    Hi @Marcin Maruszewski!

    There isn't a roadmap for this issue quite yet. However, I'm hoping that this and other TypeScript related improvements are one of our main areas of focus after we finish getting the word out about the 1.0 release.

    Helpful to know you're interested in this feature and will be following along.

  • πŸ‡΅πŸ‡±Poland Marcin Maruszewski

    While searching for a solution, I came across the https://github.com/glideapps/quicktype library. It provides a CLI for generating TypeScript interfaces/types from JSON files.

    I created a JSON file from /jsonapi/node/blog/resource/schema and ran
    quicktype --no-combine-classes --nice-property-names -o types/blog.ts schema/blog.json.

    As you can see, there are some wired interfaces like FluffyData or PurpleProperties. Also, despite the --no-combine-classes flag, some classes are combined into one, like DrupalInternalNid.

    But there are some advantages like Convert class witch to my understanding acts like a parser.

    Is this knowledge helpful for this topic?

  • Hey Marcin. Yes I think that is helpful research, thank you!

    I'm interested in looking into Zod too. Something like https://github.com/rsinohara/json-to-zod looks promising.

  • πŸ‡ΊπŸ‡ΈUnited States brianperry

    Thanks @marcin-maruszewski - any and all research is helpful at this point.

    And +1 to taking a closer look at Zod. I'm currently working on a decoupled Drupal project that uses Zod extensively. It works well, but I believe the schemas were defined manually (that work was done before I was involved). Will be interested to see if something like https://github.com/rsinohara/json-to-zod could automate that, and how far it can scale. This project has many deeply nested Paragraphs for example.

  • πŸ‡΅πŸ‡±Poland Marcin Maruszewski

    FYI, I have created this GitHub page - https://isobar-playground.github.io/jsonapi/

    It contains all the /schema routes generated with https://www.drupal.org/project/jsonapi_schema β†’ for a fresh Drupal installation with a Blog content type with field_hero_image media reference. It may be helpful in explaining what Drupal can produce in terms of JSON:API schema files. Feel free to use it.

    But TBH I think there is some confusion in creating schema with jsonapi_schema module, and output from /jsonapi pages. I used https://www.jsonschemavalidator.net/s/qpgAL5KB to validate Drupal node against generated schema and it failed. But if I just pass JSON from data then it also fails, but with some "normal" issues like missing log message or invalid type for timestamp fields. Is this how the JSON:API works?

  • πŸ‡ΊπŸ‡ΈUnited States brianperry
  • πŸ‡΅πŸ‡±Poland Marcin Maruszewski

    I also found this interesting library - https://github.com/maasglobal/io-ts-from-json-schema. But it seems that when I try to generate type from https://isobar-playground.github.io/jsonapi/action/action/resource/schema/ it fails with the following error:

    Error: properties keyword is not supported outside explicit object definitions. See https://github.com/maasglobal/io-ts-from-json-schema/issues/33

    To be honest, I'm quite sad that (according to https://dri.es/headless-cms-rest-vs-jsonapi-vs-graphql) Drupal maintainers chose JSON:API as the main headless solution and didn't check if there was a solution for this problem for such a long time (the post is from more than 5 years ago). It looks like "on paper" JSON:API looks great (and I agree), but there are no ready-made tools for PHP programmers to jump into TypeScript and get all the benefits of it, like the discussed codegen for types.

    But I also understand and appreciate the work of the people behind the API Client initiative and JSON:API. I just hope that one day there will be an out-of-the-box solution to this problem.

  • πŸ‡ΊπŸ‡ΈUnited States brianperry

    I've done some experimentation here and think this is achievable. The possible solution is quite different from what I expected though.

    I came to the same conclusion that using json-schema isn't currently viable. I'm light on some of the details here because I did some of this a few weeks back and then had to set it aside, but there were two main issues.

    * Using the jsonapi_schema module I found some of the results for resources unstable. I unfortunately can't remember the specifics, but some key resources we'd need were returning 500s. Will try to reproduce this at some point and create an issue in the jsonapi_schema project.
    * Even if that was stable, I found that packages like https://www.npmjs.com/package/json-schema-to-zod would choke on schemas with relationships due to circular references. It kind of makes sense given the JSON:API spec, but it is extremely problematic for this use case.

    So based on that, I don't see a path forward with json-schema.

    On the other hand, I found that json-to-zod works really well.

    I created a small POC here: https://github.com/backlineint/schema-gen-poc

    It actually just uses json-to-zod with the standard JSON:API endpoints to generate then schemas we need. Even more amusingly it actually uses our client to feed json into json-to-zod. Here is the entire POC script:

    import * as fs from "fs";
    import { JsonApiClient } from "@drupal-api-client/json-api-client";
    import { jsonToZod } from "json-to-zod";
    
    const client = new JsonApiClient(
      "https://dev-drupal-api-client-poc.pantheonsite.io"
    );
    const zodImport = "import { z } from 'astro:content';\n\nexport ";
    
    const writeSchema = (schema, fileName) => {
      fs.writeFile(fileName, zodImport, { flag: "w" }, (err) => {
        if (err) {
          console.error(err);
        } else {
          fs.appendFile(fileName, schema, (err) => {
            if (err) {
              console.error(err);
            } else {
              // file written successfully
            }
          });
        }
      });
    };
    
    console.log("Generating schemas...");
    
    // Generate a schema for an article collection.
    const articles = await client.getCollection("node--article", {
      queryString: "include=field_media_image.field_media_image",
    });
    const articlesSchema = jsonToZod(articles).replace(
      "const schema",
      "const articlesSchema"
    );
    writeSchema(articlesSchema, "src/lib/schemas/articlesSchema.ts");
    
    // Generate a schema for a media--image resource
    const mediaImage = await client.getCollection("media--image");
    const mediaImageSchema = jsonToZod(mediaImage.data[0]).replace(
      "const schema",
      "const mediaImageSchema"
    );
    writeSchema(mediaImageSchema, "src/lib/schemas/mediaImageSchema.ts");
    
    // Generate a schema for a file resource
    const file = await client.getCollection("file--file");
    const fileSchema = jsonToZod(file.data[0]).replace(
      "const schema",
      "const fileSchema"
    );
    writeSchema(fileSchema, "src/lib/schemas/fileSchema.ts");
    

    The intent is to run that in advance and commit the schemas to source.

    The experience of using those Zod schemas in code is really nice. This POC renders a list of articles at /blog. When sourcing article data, you'd do this:

    import { articlesSchema } from "../../lib/schemas/articlesSchema";
    
    type Articles = z.infer<typeof articlesSchema>;
    
    const articles = articlesSchema.parse(await client.getCollection<Articles>("node--article", {
      queryString: "include=field_media_image.field_media_image",
    }));
    

    Zod will throw an error when parsing if something violates the schema. You also get full intellisense/autosuggest when working with articles on the template.

    So I think this could work. What I don't know at the moment is how we could make this into a more generic utility for users. Open to any ideas. Or feedback on this approach in general.

  • I've done some experimentation here[...]

    Looks great! I would love to turn this into some sort of CLI util that eventually could be run as a step in a `create-drupal-client-app` style starter, but given the fact that the client needs to be set up first I like the idea of this being pure documentation (with examples of course) for the first iteration.

Production build 0.69.0 2024