JSONAPI Resources Anchor

Generate TypeScript types from your JSONAPI resources.

JSONAPI Resource
class CommentResource < ApplicationResource
  attribute :text
  attribute :reason
  attribute :anonymous, Anchor::Types::Boolean
  attribute :spiciness, Anchor::Types::Union.new((1..5).map { |v| Anchor::Types::Literal.new(v) })
  attribute :uninferrable

  relationship :user, to: :one, description: "Author of the comment."
  relationship :reactions, to: :many
  relationship :commentable, polymorphic: true, to: :one

  def anonymous = @model.user_id?
  def spiciness = rand(1..5)
  def uninferrable = nil
end
Generated TypeScript
import type { Post } from "./Post.model.ts";
import type { Reaction } from "./Reaction.model.ts";
import type { User } from "./User.model.ts";

type Comment = {
  id: number;
  type: "comments";
  text: string;
  /** Reason the author left a comment. */
  reason: "approval" | "disapproval" | "feedback" | null;
  anonymous: boolean;
  spiciness: 1 | 2 | 3 | 4 | 5;
  uninferrable: unknown;
  relationships: {
    /** Author of the comment. */
    user: User | null;
    reactions: Reaction[];
    commentable: Post | User;
  }
};

export { type Comment };

Features

  • Type inference
  • Type annotations
  • Single file and multifile schemas
  • Enforce your frontend schema to stay in sync with the backend via CI.
  • Support for manually editable schema files that can still be validated by CI.
  • Configurable type inference
  • Configurable serialization - customize the TypeScript serialization or even serialize to a different language/spec
  • You can create custom serializers by using the intermediate Anchor::Types representation of the resource.
  • Example: A WIP JSON Schema serializer in the repo.

Type Inference

  • Attributes are inferred from the underlying ActiveRecord model class of the resource.
  • For relationships, nullability is determined by the model's ActiveRecord reflections.
  • Type inference for attributes from RBS signatures.

From the example above:

Schema
class Comment < ActiveRecord::Base
  belongs_to :user, optional: true
  belongs_to :commentable, polymorphic: true 
  has_many :reactions

  enum :reason, { approval: "approval", disapproval: "disapproval", feedback: "feedback" }
end

ActiveRecord::Schema[8.0].define(version: 2025_10_11_000738) do
  create_table "comments", force: :cascade do |t|
    t.string "text", null: false
    t.string "reason", comment: "Reason the author left a comment."
    t.string "commentable_type"
    t.bigint "commentable_id"
    t.bigint "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["commentable_type", "commentable_id"], name: "index_comments_on_commentable"
    t.index ["user_id"], name: "index_comments_on_user_id"
  end
end
  • text - The type and JSDoc comment are derived from the database schema.
  • reason - The union is inferred from the ActiveRecord enum definition. The nullability is inferred from the database schema.
  • uninferrable - Not inferrable because it is a user defined method.
  • All user defined methods on the resource or its underlying model are considered uninferrable because the return type is not reasonably guaranteed.

    Notably, this also applies to methods that override the generated attribute methods from ActiveRecord.

  • user - Inferred as nullable because the belongs_to association is optional.

Type Annotation

In cases where the type of an attribute or relationship is not inferable (or you need to override it), you can annotate it with an Anchor::Types type.

The annotations from above:

JSONAPI Resource
class ApplicationResource < JSONAPI::Resource
  include Anchor::SchemaSerializable
  abstract
end

class CommentResource < ApplicationResource
  attribute :anonymous, Anchor::Types::Boolean
  attribute :spiciness, Anchor::Types::Union.new((1..5).map { |v| Anchor::Types::Literal.new(v) })

  def anonymous = @model.user_id?
  def spiciness = rand(1..5)
end

Playground WIP

Built with ruby/ruby.wasm and CodeMirror.
Generate TypeScript from Anchor::Types types.
Must return a string.