{"id":45892998,"url":"https://github.com/MoonWorm/jsonapi4j","last_synced_at":"2026-03-13T06:00:47.459Z","repository":{"id":319543634,"uuid":"934377492","full_name":"MoonWorm/jsonapi4j","owner":"MoonWorm","description":"Lightweight API framework for Java for building JSON:API compliant APIs with minimal configuration","archived":false,"fork":false,"pushed_at":"2026-03-10T15:30:29.000Z","size":2048,"stargazers_count":20,"open_issues_count":38,"forks_count":2,"subscribers_count":4,"default_branch":"main","last_synced_at":"2026-03-10T15:59:29.978Z","etag":null,"topics":["api","api-rest","java","json-api","oas3"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MoonWorm.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-02-17T18:18:52.000Z","updated_at":"2026-03-10T11:51:46.000Z","dependencies_parsed_at":"2025-10-19T12:22:13.782Z","dependency_job_id":"8c5a2dbc-9af5-417f-a0fe-ef2ea5cf92f6","html_url":"https://github.com/MoonWorm/jsonapi4j","commit_stats":null,"previous_names":["moonworm/jsonapi4j"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/MoonWorm/jsonapi4j","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoonWorm%2Fjsonapi4j","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoonWorm%2Fjsonapi4j/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoonWorm%2Fjsonapi4j/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoonWorm%2Fjsonapi4j/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MoonWorm","download_url":"https://codeload.github.com/MoonWorm/jsonapi4j/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MoonWorm%2Fjsonapi4j/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30459760,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-13T03:55:51.346Z","status":"ssl_error","status_checked_at":"2026-03-13T03:55:33.055Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","api-rest","java","json-api","oas3"],"created_at":"2026-02-27T19:00:39.306Z","updated_at":"2026-03-13T06:00:47.453Z","avatar_url":"https://github.com/MoonWorm.png","language":"Java","funding_links":[],"categories":["开发框架"],"sub_categories":["Web框架"],"readme":"[![Build](https://github.com/moonworm/jsonapi4j/actions/workflows/build.yml/badge.svg)](https://github.com/moonworm/jsonapi4j/actions/workflows/build.yml/badge.svg)\n[![Maven Central](https://img.shields.io/maven-central/v/pro.api4/jsonapi4j.svg?label=Maven%20Central)](https://central.sonatype.com/artifact/pro.api4/jsonapi4j)\n[![Last Commit](https://img.shields.io/github/last-commit/moonworm/jsonapi4j)](https://img.shields.io/github/last-commit/moonworm/jsonapi4j)\n[![codecov](https://codecov.io/gh/moonworm/jsonapi4j/branch/main/graph/badge.svg)](https://codecov.io/gh/moonworm/jsonapi4j)\n[![Issues](https://img.shields.io/github/issues/moonworm/jsonapi4j)](https://github.com/moonworm/jsonapi4j/issues)\n[![License](https://img.shields.io/github/license/moonworm/jsonapi4j)](LICENSE)\n\n![Logo](/docs/jsonapi4j-logo-medium.png)\n\n# Introduction\n\nWelcome to **JsonApi4j** — a lightweight API framework for Java for building [JSON:API](https://jsonapi.org/format/)-compliant APIs with minimal configuration.\n\nThere are some **application examples** available in [examples/](https://github.com/MoonWorm/jsonapi4j/tree/main/examples) folder. Please check them out for more insights on how to use the framework.\n\nDetailed **documentation** is available [here](https://moonworm.github.io/jsonapi4j/).\n\n# Quick start\n\nLet's take a quick look at what a typical **JsonApi4j**-based application looks like in code.  \nAs an example, we'll integrate **JsonApi4j** into a clean or existing [Spring Boot](https://spring.io/projects/spring-boot) application.\n\n## 1. Add Dependency\n\n### Maven\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003epro.api4\u003c/groupId\u003e\n  \u003cartifactId\u003ejsonapi4j-rest-springboot\u003c/artifactId\u003e\n  \u003cversion\u003e${jsonapi4j.version}\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n### Gradle\n```groovy\nimplementation \"pro.api4:jsonapi4j-rest-springboot:${jsonapi4jVersion}\"\n```\n\nThe framework modules are published to Maven Central. You can find the latest available versions [here](https://mvnrepository.com/artifact/pro.api4).\n\n## 2. Declare the Domain\n\nLet's implement a simple application that exposes just one resource - `users`. And `relatives` relationship that is self-linked on the same `users` resource.\n\n```mermaid\nflowchart TD\n    U[users] --\u003e|relatives| U\n```\n\nThen, let's implement a few operations - reading multiple `users`, and retrieving which are the other `users` the current user has as `relatives`.\n\n## 3. Define the JSON:API Resource for Users\n\nAs mentioned above, let's start by defining our first JSON:API resource - `user` resource.\n\n```java\n@JsonApiResource(resourceType = \"users\") // 1.\npublic class UserResource implements Resource\u003cUserDbEntity\u003e { // 2.\n\n    // 3. \n    @Override\n    public String resolveResourceId(UserDbEntity userDbEntity) {\n      return userDbEntity.getId();\n    }\n  \n    // 4.\n    @Override\n    public UserAttributes resolveAttributes(UserDbEntity userDbEntity) {\n      return new UserAttributes(\n              userDbEntity.getFirstName() + \" \" + userDbEntity.getLastName(),\n              userDbEntity.getEmail(),\n              userDbEntity.getCreditCardNumber()\n      );\n    }\n\n}\n```\n\nWhat's happening here:\n1. `@JsonApiResource(resourceType = \"users\")` defines a unique resource type name (`users` in this case). Each resource in your API must have a distinct type.\n2. `UserResource implements Resource\u003cUserDbEntity\u003e` each resource must implement `Resource` interface. It's parametrized with a `UserDbEntity` type - is how data is represented internally.\n3. `String resolveResourceId(UserDbEntity userDbEntity)` returns the unique identifier for this resource, must be unique across all resources of this type.\n4. `UserAttributes resolveAttributes(UserDbEntity userDbEntity)` - (optional) maps internal domain data (`UserDbEntity`) to the public API-facing representation (`UserAttributes`)\n\nHere's a draft implementation of `UserAttributes`:\n\n```java\npublic class UserAttributes {\n    \n    private final String firstName;\n    private final String lastName;\n    private final String email;\n    private final String creditCardNumber;\n    \n    // constructors, getters and setters\n\n}\n```\n\nand `UserDbEntity`:\n\n```java\npublic class UserDbEntity {\n\n    private final String id;\n    private final String fullName;\n    private final String email;\n    private final String creditCardNumber;\n    \n    // constructors, getters and setters\n\n}\n```\n\nInternal models (like `UserDbEntity` in this case) often differ from `UserAttributes`. They may encapsulate database-specific details (for example, a Hibernate entity or a JOOQ record), represent a DTO from an external service, or even aggregate data from multiple sources.\n\n## 4. Declare the first JSON:API Operation — Read Multiple Users\n\nNow that we've defined our resource and attributes, let's implement the first operation to read all users.\nThis operation will be available under `GET /users`.\n\n```java\n@JsonApiResourceOperation(resource = UserResource.class) // 1.\npublic class UserOperations implements ResourceOperations\u003cUserDbEntity\u003e { // 2.\n\n    private final UserDb userDb; // 3.\n    \n    public UserOperations(UserDb userDb) {\n        this.userDb = userDb;\n    }\n\n    @Override\n    public CursorPageableResponse\u003cUserDbEntity\u003e readPage(JsonApiRequest request) { // 4.\n        UserDb.DbPage\u003cUserDbEntity\u003e pagedResult = userDb.readAllUsers(request.getCursor());\n        return CursorPageableResponse.fromItemsAndCursor(\n                pagedResult.getEntities(),\n                pagedResult.getCursor()\n        );\n    }\n\n}\n```\n\nWhat's happening here:\n1. `@JsonApiResourceOperation(resource = UserResource.class)` - identify which JSON:API resource this operation belongs to (`users`).\n2. `UserOperations implements ResourceOperations\u003cUserDbEntity\u003e` - this class must implement `ResourceOperations`. This interface consist of all available operations that can be implemented for any JSON:API resource. Interface parametrized with `UserDbEntity` - internal model that represents our `users` resource. \n3. The `UserDb` class doesn't depend on any **JsonApi4j**-specific interfaces or components — it simply represents your data source. In a real application, this could be an ORM entity manager, a JOOQ repository, a REST client, or any other persistence mechanism. \n4. As of now we only implement `readPage(...)` method among others available in `ResourceOperations`. \n\nFor the sake of this demo, here’s a simple in-memory implementation of `UserDb` to support the operations from above:\n```java\npublic class UserDb {\n\n    private Map\u003cString, UserDbEntity\u003e users = new ConcurrentHashMap\u003c\u003e();\n    {\n        users.put(\"1\", new UserDbEntity(\"1\", \"John Doe\", \"john@doe.com\", \"123456789\"));\n        users.put(\"2\", new UserDbEntity(\"2\", \"Jane Doe\", \"jane@doe.com\", \"222456789\"));\n        users.put(\"3\", new UserDbEntity(\"3\", \"Jack Doe\", \"jack@doe.com\", \"333456789\"));\n        users.put(\"4\", new UserDbEntity(\"4\", \"Jessy Doe\", \"jessy@doe.com\", \"444456789\"));\n        users.put(\"5\", new UserDbEntity(\"5\", \"Jared Doe\", \"jared@doe.com\", \"555456789\"));\n    }\n\n    public DbPage\u003cUserDbEntity\u003e readAllUsers(String cursor) {\n        LimitOffsetToCursorAdapter adapter = new LimitOffsetToCursorAdapter(cursor).withDefaultLimit(2); // let's say our page size is 2\n        LimitOffsetToCursorAdapter.LimitAndOffset limitAndOffset = adapter.decodeLimitAndOffset();\n\n        int effectiveFrom = limitAndOffset.getOffset() \u003c users.size() ? limitAndOffset.getOffset() : users.size() - 1;\n        int effectiveTo = Math.min(effectiveFrom + limitAndOffset.getLimit(), users.size());\n\n        List\u003cUserDbEntity\u003e result = new ArrayList\u003c\u003e(users.values()).subList(effectiveFrom, effectiveTo);\n        String nextCursor = adapter.nextCursor(users.size());\n        return new DbPage\u003c\u003e(nextCursor, result);\n    }\n\n    public static class DbPage\u003cE\u003e {\n\n        private final String cursor;\n        private final List\u003cE\u003e entities;\n\n        public DbPage(String cursor, List\u003cE\u003e entities) {\n            this.cursor = cursor;\n            this.entities = entities;\n        }\n\n        public String getCursor() {\n            return cursor;\n        }\n\n        public List\u003cE\u003e getEntities() {\n            return entities;\n        }\n    }\n}\n```\n\nYou can now run your application (for example, on port `8080` by setting Spring Boot's property to `server.port=8080`) and send the next HTTP request: [/users?page[cursor]=DoJu](http://localhost:8080/jsonapi/users?page[cursor]=DoJu).\n\nAnd then you should receive a paginated, JSON:API-compliant response such as:\n\n\u003cdetails\u003e\n\u003csummary\u003eResponse\u003c/summary\u003e\n\n```json\n{\n  \"data\": [\n    {\n      \"attributes\": {\n        \"fullName\": \"Jack Doe\",\n        \"email\": \"jack@doe.com\",\n        \"creditCardNumber\": \"333456789\"\n      },\n      \"links\": {\n        \"self\": \"/users/3\"\n      },\n      \"id\": \"3\",\n      \"type\": \"users\"\n    },\n    {\n      \"attributes\": {\n        \"fullName\": \"Jessy Doe\",\n        \"email\": \"jessy@doe.com\",\n        \"creditCardNumber\": \"444456789\"\n      },\n      \"links\": {\n        \"self\": \"/users/4\"\n      },\n      \"id\": \"4\",\n      \"type\": \"users\"\n    }\n  ],\n  \"links\": {\n    \"self\": \"/users?page%5Bcursor%5D=DoJu\",\n    \"next\": \"/users?page%5Bcursor%5D=DoJw\"\n  }\n}\n```\n\n\u003c/details\u003e\n\nTry to remove `page[cursor]=xxx` query parameter - it will just start reading user resources from the very beginning.\n\n## 5. Add a JSON:API Relationship - User Relatives\n\nNow that we've defined our first resource, let's establish a `relatives` relationship between `users`.\n\nEach user can have multiple `relatives`, which makes this a **to-many** relationship (represented by an array of resource identifier objects).\n\nTo implement this, we'll create a class that implements the `ToManyRelationship` interface:\n\n```java\n@JsonApiRelationship(relationshipName = \"relatives\", parentResource = UserResource.class) // 1.\npublic class UserRelativesRelationship implements ToManyRelationship\u003cUserRelationshipInfo\u003e { // 2.\n\n    @Override // 3.\n    public String resolveResourceIdentifierType(UserRelationshipInfo userRelationshipInfo) {\n        return \"users\";\n    }\n\n    @Override // 4.\n    public String resolveResourceIdentifierId(UserRelationshipInfo userRelationshipInfo) {\n        return userRelationshipInfo.getRelativeUserId();\n    }\n\n    @Override // 5.\n    public Object resolveResourceIdentifierMeta(JsonApiRequest relationshipRequest, UserRelationshipInfo userRelationshipInfo) {\n        return Map.of(\"relationshipType\", userRelationshipInfo.getRelationshipType());\n    }\n\n}\n```\n\n1. `@JsonApiRelationship(relationshipName = \"relatives\", parentResource = UserResource.class)` - defines the name of the relationship - `relatives` and identifies which resource this relationship belongs to - `UserResource`.\n2. `UserRelativesRelationship implements ToManyRelationship\u003cUserRelationshipInfo\u003e` - this relationship must implement `ToManyRelationship` interface because it has 'to-many' nature. Interface is parametrized with a type - internal model that represents relationship linkage - `UserRelationshipInfo`.   \n3. `String resolveResourceIdentifierType(UserRelationshipInfo userRelationshipInfo)` - determines the type of the related resource - `users`. In some cases, a relationship may include multiple resource types - for example, a `userProperty` relationship could contain a mix of `cars`, `apartments`, or `yachts`.\n4. `String resolveResourceIdentifierId(UserRelationshipInfo userRelationshipInfo)` - resolves the unique identifier of the user.\n5. `Object resolveResourceIdentifierMeta(JsonApiRequest relationshipRequest, UserRelationshipInfo userRelationshipInfo)` - optional, we can place the information about the nature of relationships between two people into 'meta' object of the JSON:API Resource Identifier. Please refer the [JSON:API specification](https://jsonapi.org/format/#document-resource-identifier-objects) for more details.\n\n## 6. Add the Missing Relationship Operation\n\nThe final piece of the puzzle is teaching the framework how to **resolve the declared relationship data**.\n\nTo do this, implement `UserRelativesOperations` - this tells **JsonApi4j** how to find the related user relatives.\n\n```java\n@JsonApiRelationshipOperation(relationship = UserRelativesRelationship.class) // 1.\npublic class UserRelativesOperations implements ToManyRelationshipOperations\u003cUserDbEntity, UserRelationshipInfo\u003e { // 2.\n    \n    private final UserDb userDb;\n    \n    public UserRelativesOperations(UserDb userDb) {\n        this.userDb = userDb;\n    }\n    \n    @Override\n    public CursorPageableResponse\u003cUserRelationshipInfo\u003e readMany(JsonApiRequest request) { // 3.\n        return CursorPageableResponse.fromItemsPageable(\n                userDb.getUserRelatives(request.getResourceId()),\n                request.getCursor(),\n                2 // 4.\n        ); \n    }\n    \n}\n```\n\n1. `@JsonApiRelationshipOperation(relationship = UserRelativesRelationship.class)` identifies which relationship this operation belongs to.\n2. `UserRelativesOperations implements ToManyRelationshipOperations\u003cUserDbEntity, UserRelationshipInfo\u003e` - this class must implement `ToManyRelationshipOperations` interface because it has 'to-many' nature. Interface is parametrized with two types: internal model of the parent resource (`UserDbEntity`) and internal model that represents relationship resource (`UserRelationshipInfo`).\n3. `CursorPageableResponse\u003cUserRelationshipInfo\u003e readMany(JsonApiRequest request)` - As of now we only implement `readMany(...)` method among others available in `ToManyRelationshipOperations`.\n4. Let's set page size to 2 in order to showcase the pagination\n\nHere is the internal modal that represents user relatives linkage:\n```java\npublic class UserRelationshipInfo {\n\n    private final String relativeUserId;\n    private final RelationshipType relationshipType;\n\n    public enum RelationshipType {\n        HUSBAND, WIFE, SON, DAUGHTER, MOTHER, FATHER, BROTHER\n    }\n    \n    // getters/setters/...\n}\n```\n\nWe also need to extend our existing `UserDb` to include information about user relatives.\n```java\n\npublic class UserDb {\n    \n    //  ...\n\n    private Map\u003cString, List\u003cUserRelationshipInfo\u003e\u003e userRalatives = new ConcurrentHashMap\u003c\u003e();\n    {\n        userRalatives.put(\n                \"1\",\n                List.of(\n                        new UserRelationshipInfo(\"2\", RelationshipType.HUSBAND),\n                        new UserRelationshipInfo(\"3\", RelationshipType.BROTHER)\n                )\n        );\n        userRalatives.put(\n                \"2\",\n                List.of(\n                        new UserRelationshipInfo(\"1\", RelationshipType.WIFE),\n                        new UserRelationshipInfo(\"4\", RelationshipType.SON)\n                )\n        );\n        userRalatives.put(\"3\", Collections.emptyList());\n        userRalatives.put(\n                \"4\",\n                List.of(\n                        new UserRelationshipInfo(\"1\", RelationshipType.FATHER),\n                        new UserRelationshipInfo(\"2\", RelationshipType.MOTHER)\n                )\n        );\n        userRalatives.put(\n                \"5\",\n                List.of(\n                        new UserRelationshipInfo(\"1\", RelationshipType.BROTHER),\n                        new UserRelationshipInfo(\"2\", RelationshipType.DAUGHTER),\n                        new UserRelationshipInfo(\"3\", RelationshipType.FATHER),\n                        new UserRelationshipInfo(\"4\", RelationshipType.BROTHER)\n                )\n        );\n    }\n\n    public List\u003cUserRelationshipInfo\u003e getUserRelatives(String userId) {\n        return userRalatives.get(userId);\n    }\n\n    // ...\n\n}\n```\n\nFinally, this operation will be available under `GET /users/{userId}/relationships/relatives`.\n\n## 7. Request/Response Examples\n\n### Fetch a User's Relatives Relationships\n\nRequest: [/users/5/relationships/relatives](http://localhost:8080/jsonapi/users/5/relationships/relatives)\n\n\u003cdetails\u003e\n\u003csummary\u003eResponse\u003c/summary\u003e\n\n```json\n{\n  \"links\": {\n    \"self\": \"/users/5/relationships/relatives\",\n    \"related\": {\n      \"users\": {\n        \"href\": \"/users?filter[id]=1,2\",\n        \"describedby\": \"https://api4.pro/oas-schema-to-many-relationships-related-link.yaml\",\n        \"meta\": {\n          \"ids\": [\n            \"1\",\n            \"2\"\n          ]\n        }\n      }\n    },\n    \"next\": \"/users/5/relationships/relatives?page%5Bcursor%5D=DoJu\"\n  },\n  \"data\": [\n    {\n      \"id\": \"1\",\n      \"type\": \"users\",\n      \"meta\": {\n        \"relationshipType\": \"BROTHER\"\n      }\n    },\n    {\n      \"id\": \"2\",\n      \"type\": \"users\",\n      \"meta\": {\n        \"relationshipType\": \"DAUGHTER\"\n      }\n    }\n  ]\n}\n```\n\n\u003c/details\u003e\n\nIt's worth noting that 'relatives' relationship has its own pagination. The link to the next page can be found in the response under `links` -\u003e `next`.\n\nFor example, to fetch the second page of a user's relatives relationship, try:\n[/users/5/relationships/relatives?page%5Bcursor%5D=DoJu](http://localhost:8080/jsonapi/users/5/relationships/relatives?page%5Bcursor%5D=DoJu)\n\n### Fetch a User's Relatives Relationships Along with Corresponding User Resources\n\nRequest: [/users/5/relationships/relatives?include=relatives](http://localhost:8080/jsonapi/users/5/relationships/relatives?include=relatives)\n\n\u003cdetails\u003e\n\u003csummary\u003eResponse\u003c/summary\u003e\n\n```json\n{\n  \"links\": {\n    \"self\": \"/users/5/relationships/relatives?include=relatives\",\n    \"related\": {\n      \"users\": {\n        \"href\": \"/users?filter[id]=1,2\",\n        \"describedby\": \"https://api4.pro/oas-schema-to-many-relationships-related-link.yaml\",\n        \"meta\": {\n          \"ids\": [\n            \"1\",\n            \"2\"\n          ]\n        }\n      }\n    },\n    \"next\": \"/users/5/relationships/relatives?include=relatives\u0026page%5Bcursor%5D=DoJu\"\n  },\n  \"data\": [\n    {\n      \"id\": \"1\",\n      \"type\": \"users\",\n      \"meta\": {\n        \"relationshipType\": \"BROTHER\"\n      }\n    },\n    {\n      \"id\": \"2\",\n      \"type\": \"users\",\n      \"meta\": {\n        \"relationshipType\": \"DAUGHTER\"\n      }\n    }\n  ],\n  \"included\": [\n    {\n      \"id\": \"1\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"John Doe\",\n        \"email\": \"john@doe.com\",\n        \"creditCardNumber\": \"123456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/1/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/1\"\n      }\n    },\n    {\n      \"id\": \"2\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jane Doe\",\n        \"email\": \"jane@doe.com\",\n        \"creditCardNumber\": \"222456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/2/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/2\"\n      }\n    }\n  ]\n}\n```\n\n\u003c/details\u003e\n\n### Fetch Multiple Users by IDs\n\nRequest: [/users?filter[id]=1,2](http://localhost:8080/jsonapi/users?filter[id]=1,2)\n\n\u003cdetails\u003e\n\u003csummary\u003eResponse\u003c/summary\u003e\n\n```json\n{\n  \"links\": {\n    \"self\": \"/users?filter%5Bid%5D=1%2C2\"\n  },\n  \"data\": [\n    {\n      \"id\": \"1\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"John Doe\",\n        \"email\": \"john@doe.com\",\n        \"creditCardNumber\": \"123456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/1/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/1\"\n      }\n    },\n    {\n      \"id\": \"2\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jane Doe\",\n        \"email\": \"jane@doe.com\",\n        \"creditCardNumber\": \"222456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/2/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/2\"\n      }\n    }\n  ]\n}\n```\n\n\u003c/details\u003e\n\n### Fetch a Specific Page of Users with Relatives\n\nRequest: [/users?page[cursor]=DoJu\u0026include=relatives](http://localhost:8080/jsonapi/users?page[cursor]=DoJu\u0026include=relatives)\n\n\u003cdetails\u003e\n\u003csummary\u003eResponse\u003c/summary\u003e\n\n```json\n{\n  \"links\": {\n    \"self\": \"/users?include=relatives\u0026page%5Bcursor%5D=DoJu\",\n    \"next\": \"/users?include=relatives\u0026page%5Bcursor%5D=DoJw\"\n  },\n  \"data\": [\n    {\n      \"id\": \"3\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jack Doe\",\n        \"email\": \"jack@doe.com\",\n        \"creditCardNumber\": \"333456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/3/relationships/relatives\",\n            \"related\": {}\n          },\n          \"data\": []\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/3\"\n      }\n    },\n    {\n      \"id\": \"4\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jessy Doe\",\n        \"email\": \"jessy@doe.com\",\n        \"creditCardNumber\": \"444456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/4/relationships/relatives\",\n            \"related\": {\n              \"users\": {\n                \"href\": \"/users?filter[id]=1,2\",\n                \"describedby\": \"https://api4.pro/oas-schema-to-many-relationships-related-link.yaml\",\n                \"meta\": {\n                  \"ids\": [\n                    \"1\",\n                    \"2\"\n                  ]\n                }\n              }\n            }\n          },\n          \"data\": [\n            {\n              \"id\": \"1\",\n              \"type\": \"users\",\n              \"meta\": {\n                \"relationshipType\": \"FATHER\"\n              }\n            },\n            {\n              \"id\": \"2\",\n              \"type\": \"users\",\n              \"meta\": {\n                \"relationshipType\": \"MOTHER\"\n              }\n            }\n          ]\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/4\"\n      }\n    }\n  ],\n  \"included\": [\n    {\n      \"id\": \"2\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jane Doe\",\n        \"email\": \"jane@doe.com\",\n        \"creditCardNumber\": \"222456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/2/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/2\"\n      }\n    },\n    {\n      \"id\": \"1\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"John Doe\",\n        \"email\": \"john@doe.com\",\n        \"creditCardNumber\": \"123456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/1/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/1\"\n      }\n    }\n  ]\n}\n```\n\n\u003c/details\u003e\n\nUser '3' has no relatives. While user '4' has one relative with id '1' and '2'. The corresponding user resource can be found in the \"included\" section.\n\n### Fetch a Specific Page of Users with Relatives and their Relatives\n\nNow let's read the same users, but with 2-levels of relatives, e.g. with relatives of their relatives.\n\nRequest: [/users?page[cursor]=DoJu\u0026include=relatives.relatives](http://localhost:8080/jsonapi/users?page[cursor]=DoJu\u0026include=relatives.relatives)\n\n\u003cdetails\u003e\n\u003csummary\u003eResponse\u003c/summary\u003e\n\n```json\n{\n  \"links\": {\n    \"self\": \"/users?include=relatives\u0026page%5Bcursor%5D=DoJu\",\n    \"next\": \"/users?include=relatives\u0026page%5Bcursor%5D=DoJw\"\n  },\n  \"data\": [\n    {\n      \"id\": \"3\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jack Doe\",\n        \"email\": \"jack@doe.com\",\n        \"creditCardNumber\": \"333456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/3/relationships/relatives\",\n            \"related\": {}\n          },\n          \"data\": []\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/3\"\n      }\n    },\n    {\n      \"id\": \"4\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jessy Doe\",\n        \"email\": \"jessy@doe.com\",\n        \"creditCardNumber\": \"444456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/4/relationships/relatives\",\n            \"related\": {\n              \"users\": {\n                \"href\": \"/users?filter[id]=1,2\",\n                \"describedby\": \"https://api4.pro/oas-schema-to-many-relationships-related-link.yaml\",\n                \"meta\": {\n                  \"ids\": [\n                    \"1\",\n                    \"2\"\n                  ]\n                }\n              }\n            }\n          },\n          \"data\": [\n            {\n              \"id\": \"1\",\n              \"type\": \"users\",\n              \"meta\": {\n                \"relationshipType\": \"FATHER\"\n              }\n            },\n            {\n              \"id\": \"2\",\n              \"type\": \"users\",\n              \"meta\": {\n                \"relationshipType\": \"MOTHER\"\n              }\n            }\n          ]\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/4\"\n      }\n    }\n  ],\n  \"included\": [\n    {\n      \"id\": \"2\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jane Doe\",\n        \"email\": \"jane@doe.com\",\n        \"creditCardNumber\": \"222456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/2/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/2\"\n      }\n    },\n    {\n      \"id\": \"4\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jessy Doe\",\n        \"email\": \"jessy@doe.com\",\n        \"creditCardNumber\": \"444456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/4/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/4\"\n      }\n    },\n    {\n      \"id\": \"1\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"John Doe\",\n        \"email\": \"john@doe.com\",\n        \"creditCardNumber\": \"123456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/1/relationships/relatives\",\n            \"related\": {\n              \"users\": {\n                \"href\": \"/users?filter[id]=2,3\",\n                \"describedby\": \"https://api4.pro/oas-schema-to-many-relationships-related-link.yaml\",\n                \"meta\": {\n                  \"ids\": [\n                    \"2\",\n                    \"3\"\n                  ]\n                }\n              }\n            }\n          },\n          \"data\": [\n            {\n              \"id\": \"2\",\n              \"type\": \"users\",\n              \"meta\": {\n                \"relationshipType\": \"HUSBAND\"\n              }\n            },\n            {\n              \"id\": \"3\",\n              \"type\": \"users\",\n              \"meta\": {\n                \"relationshipType\": \"BROTHER\"\n              }\n            }\n          ]\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/1\"\n      }\n    },\n    {\n      \"id\": \"2\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jane Doe\",\n        \"email\": \"jane@doe.com\",\n        \"creditCardNumber\": \"222456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/2/relationships/relatives\",\n            \"related\": {\n              \"users\": {\n                \"href\": \"/users?filter[id]=1,4\",\n                \"describedby\": \"https://api4.pro/oas-schema-to-many-relationships-related-link.yaml\",\n                \"meta\": {\n                  \"ids\": [\n                    \"1\",\n                    \"4\"\n                  ]\n                }\n              }\n            }\n          },\n          \"data\": [\n            {\n              \"id\": \"1\",\n              \"type\": \"users\",\n              \"meta\": {\n                \"relationshipType\": \"WIFE\"\n              }\n            },\n            {\n              \"id\": \"4\",\n              \"type\": \"users\",\n              \"meta\": {\n                \"relationshipType\": \"SON\"\n              }\n            }\n          ]\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/2\"\n      }\n    },\n    {\n      \"id\": \"1\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"John Doe\",\n        \"email\": \"john@doe.com\",\n        \"creditCardNumber\": \"123456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/1/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/1\"\n      }\n    },\n    {\n      \"id\": \"3\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jack Doe\",\n        \"email\": \"jack@doe.com\",\n        \"creditCardNumber\": \"333456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/3/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/3\"\n      }\n    }\n  ]\n}\n```\n\n\u003c/details\u003e\n\nUser '3' has no relatives. While user '4' has one relative with id '1' and '2'. User '1' has relatives '2' and '3' and user '2' has relatives '1' and '4'. All the mentioned user resources can be found in the \"included\" section.\n\n# Advanced features\n\nLet's proceed with the domain from the above and showcase few more features.\n\n## Access Control Plugin\n\nLet's extend our domain model by restricting access to `users` resources for non-authenticated users. In addition to that we're going to restrict access to `creditCardNumber` field in `UserAttributes` for non-resource owners.  \n\nIn order to achieve that we just need to place `@AccessControl` annotation on top of `UserAttributes` class and above `creditCardNumber` field like that: \n\n```java\n@AccessControl(authenticated = Authenticated.AUTHENTICATED) // 1.\npublic class UserAttributes {\n    \n    private final String fullName;\n    private final String email;\n\n    // 2.\n    @AccessControl(\n            scopes = @AccessControlScopes(requiredScopes = \"users.sensitive.read\"),\n            ownership = @AccessControlOwnership(ownerIdFieldPath = \"id\")\n    )\n    private final String creditCardNumber;\n\n}\n```\n\n1. `@AccessControl(authenticated = Authenticated.AUTHENTICATED)` on a class level restricts access to the entire JSON:API Attributes Object for all non-authenticated users.\n2. `@AccessControl(scopes = @AccessControlScopes(requiredScopes = \"users.sensitive.read\"), ownership = @AccessControlOwnership(ownerIdFieldPath = \"id\"))` on a `creditCardNumber` field level reveal the value for users that: 1. The owners of the corresponding `users` resource 2. Have granted `users.sensitive.read` OAuth2 scope to the requesting app.\n\nThus, if we send the next request: [/users?filter[id]=4,5](http://localhost:8080/jsonapi/users?filter[id]=4,5) without any HTTP Headers we will the next response:\n\n\u003cdetails\u003e\n\u003csummary\u003eResponse\u003c/summary\u003e\n\n```json\n{\n    \"links\": {\n        \"self\": \"/users?filter%5Bid%5D=4%2C5\"\n    },\n    \"data\": [\n        {\n            \"id\": \"4\",\n            \"type\": \"users\",\n            \"relationships\": {\n                \"relatives\": {\n                    \"links\": {\n                        \"self\": \"/users/4/relationships/relatives\"\n                    }\n                }\n            },\n            \"links\": {\n                \"self\": \"/users/4\"\n            }\n        },\n        {\n            \"id\": \"5\",\n            \"type\": \"users\",\n            \"relationships\": {               \n                \"relatives\": {\n                    \"links\": {\n                        \"self\": \"/users/5/relationships/relatives\"\n                    }\n                }\n            },\n            \"links\": {\n                \"self\": \"/users/5\"\n            }\n        }\n    ]\n}\n```\n\n\u003c/details\u003e\n\nThere is no visible `attributes` sections because the request was made on behalf of unauthenticated user. \n\nLet's add two HTTP Headers: \n1. `X-Authenticated-User-Id: 5`\n2. `X-Authenticated-User-Granted-Scopes: users.sensitive.read`\n\nAnd the response for the same request now looks like:\n\n\u003cdetails\u003e\n\u003csummary\u003eResponse\u003c/summary\u003e\n\n```json\n{\n  \"links\": {\n    \"self\": \"/users?filter%5Bid%5D=4%2C5\"\n  },\n  \"data\": [\n    {\n      \"id\": \"4\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jessy Doe\",\n        \"email\": \"jessy@doe.com\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/4/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/4\"\n      }\n    },\n    {\n      \"id\": \"5\",\n      \"type\": \"users\",\n      \"attributes\": {\n        \"fullName\": \"Jared Doe\",\n        \"email\": \"jared@doe.com\",\n        \"creditCardNumber\": \"555456789\"\n      },\n      \"relationships\": {\n        \"relatives\": {\n          \"links\": {\n            \"self\": \"/users/5/relationships/relatives\"\n          }\n        }\n      },\n      \"links\": {\n        \"self\": \"/users/5\"\n      }\n    }\n  ]\n}\n```\n\n\u003c/details\u003e\n\nIt's worth noting that we can now see `attributes` sections for both requested users. But for user '5' (authenticated user) we can also see `creditCardNumber`. \n\nFor more configuration options like setting a custom principal resolver or other examples for setting access control rules for both inbound and outbound access please refer the official [documentation](https://api4.pro/).  \n\n## OpenApi Plugin\n\nJsonApi4j has OpenAPI Specification support by default. \n\nYou can get OpenAPI Specification in desired format (json or yaml) by accessing [/oas](http://localhost:8080/jsonapi/oas) URL. \n\nHere is the Swagger UI you can generate based on the OpenAPI Specification by doing zero configuration: \n\n![Swagger UI](/docs/swagger-ui-screenshot.png)\n\nJsonApi4j generates JSON:API parameters and a full set of Schemas based on the declared domain.\n\nFor more configuration options and details please refer the official [documentation](https://api4.pro/).\n\n# Contributing \n\nI welcome issues and pull requests! See [CONTRIBUTING.md](./CONTRIBUTING.md) for details.\n\n# License \n\nThis project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMoonWorm%2Fjsonapi4j","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FMoonWorm%2Fjsonapi4j","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FMoonWorm%2Fjsonapi4j/lists"}