JSONAPI Resources Anchor
Generate TypeScript types from your JSONAPI resources.
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
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:
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 theActiveRecord
enum definition. The nullability is inferred from the database schema.uninferrable
- Not inferrable because it is a user defined method.user
- Inferred as nullable because thebelongs_to
association is optional.
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
.
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:
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.