diff --git a/README.md b/README.md
index 9ce4d62..73249d7 100644
--- a/README.md
+++ b/README.md
@@ -2,3 +2,79 @@
[](https://codecov.io/gh/OdunlamiZO/java-jsonbin)
+A minimal and type-safe Java SDK for interacting with JsonBin.io. This SDK allows developers to read and deserialize JSON documents from public or private bins using a clean, strongly-typed API.
+
+## Requirements
+
+- Java 17 or higher
+- Maven (for dependency management)
+
+## Installation
+
+1. Add the GitHub Maven repository to your `pom.xml`:
+
+```xml
+
+
+ github
+ GitHub Packages - java-jsonbin
+ https://maven.pkg.github.com/OdunlamiZO/java-jsonbin
+
+
+```
+
+2. Add the dependency:
+
+```xml
+
+ io.github.odunlamizo
+ java-jsonbin
+ 1.0-SNAPSHOT
+
+```
+
+3. Configure GitHub credentials in your Maven settings.xml (usually located at ~/.m2/settings.xml):
+
+```xml
+
+
+
+ github
+ YOUR_GITHUB_USERNAME
+ YOUR_PERSONAL_ACCESS_TOKEN
+
+
+
+```
+
+> 📌 Step 1 & 3 is required for now, since we are only deploying to github packages.
+
+## Getting Started
+
+### Setup
+
+```java
+JsonBin jsonBin =
+ new JsonBinOkHttp.Builder().withMasterKey("JSONBIN_MASTER_KEY").build(UserList.class);
+```
+
+### Example: Calling the API
+
+Here's a basic example of using the SDK to read a bin from JSONBin.io:
+
+```java
+Bin bin = jsonBin.readBin("687644d36063391d31ae163f");
+System.out.println(bin);
+// Bin(record={users=[{name=Morounfoluwa Mary, age=19}]}, metadata=Metadata(id=687644d36063391d31ae163f, _private=false, createdAt=2025-07-15T12:08:51.887Z, name=Java SDK Test))
+```
+
+## Contributing
+
+We welcome contributions to improve this SDK! To contribute:
+
+1. **Fork** the repository.
+2. **Create a new branch** for your feature or bugfix.
+3. Make your changes and write appropriate tests.
+4. **Open a Pull Request (PR)** to the `main` branch with a clear description of your changes.
+
+> 📌 Please ensure your code adheres to the project's style and passes all tests before submitting a PR.
\ No newline at end of file
diff --git a/src/main/java/io/github/odunlamizo/jsonbin/App.java b/src/main/java/io/github/odunlamizo/jsonbin/App.java
index 06c1e4e..a859a73 100644
--- a/src/main/java/io/github/odunlamizo/jsonbin/App.java
+++ b/src/main/java/io/github/odunlamizo/jsonbin/App.java
@@ -20,7 +20,8 @@ public static void main(String[] args) {
System.exit(1);
}
- JsonBin jsonBin = new JsonBinOkHttp<>(masterKey);
+ JsonBin jsonBin =
+ new JsonBinOkHttp.Builder().withMasterKey(masterKey).build(UserList.class);
Bin bin = jsonBin.readBin("687644d36063391d31ae163f");
System.out.println(bin);
}
diff --git a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java
index d887fa2..4172b67 100644
--- a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java
+++ b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/AuthInterceptor.java
@@ -20,15 +20,13 @@ public AuthInterceptor(String masterKey, String accessKey) {
public Response intercept(Chain chain) throws IOException {
Request original = chain.request();
- boolean hasAccessKey = accessKey != null && !accessKey.isBlank();
-
- Request requestWithAuth =
- original.newBuilder()
- .header(
- hasAccessKey ? "x-access-key" : "x-master-key",
- hasAccessKey ? accessKey : masterKey)
- .build();
-
- return chain.proceed(requestWithAuth);
+ Request.Builder requestBuilder = original.newBuilder();
+ if (accessKey != null && !accessKey.isEmpty()) {
+ requestBuilder.header("x-access-key", accessKey);
+ } else {
+ requestBuilder.header("x-master-key", masterKey);
+ }
+
+ return chain.proceed(requestBuilder.build());
}
}
diff --git a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java
index e541579..88b45cc 100644
--- a/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java
+++ b/src/main/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttp.java
@@ -1,12 +1,15 @@
package io.github.odunlamizo.jsonbin.okhttp;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.type.TypeFactory;
import io.github.odunlamizo.jsonbin.JsonBin;
import io.github.odunlamizo.jsonbin.JsonBinException;
import io.github.odunlamizo.jsonbin.model.Bin;
import io.github.odunlamizo.jsonbin.model.Error;
import io.github.odunlamizo.jsonbin.util.JsonUtil;
import java.io.IOException;
+import java.util.function.Function;
import lombok.NonNull;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -15,24 +18,73 @@
public class JsonBinOkHttp implements JsonBin {
private final OkHttpClient client;
-
private final String baseUrl;
+ private final Function> deserializer;
- private final TypeReference> typeReference;
-
- public JsonBinOkHttp(@NonNull String masterKey) {
- this(masterKey, "https://api.jsonbin.io/v3");
- }
-
- public JsonBinOkHttp(@NonNull String masterKey, @NonNull String baseUrl) {
+ private JsonBinOkHttp(
+ String masterKey,
+ String accessKey,
+ String baseUrl,
+ Function> deserializer) {
this.baseUrl = baseUrl;
- this.typeReference = new TypeReference>() {};
+ this.deserializer = deserializer;
this.client =
new OkHttpClient.Builder()
- .addInterceptor(new AuthInterceptor(masterKey, null))
+ .addInterceptor(new AuthInterceptor(masterKey, accessKey))
.build();
}
+ public static class Builder {
+ private String masterKey;
+
+ private String accessKey;
+
+ private String baseUrl = "https://api.jsonbin.io/v3";
+
+ public Builder withMasterKey(String masterKey) {
+ this.masterKey = masterKey;
+ return this;
+ }
+
+ public Builder withAccessKey(String accessKey) {
+ this.accessKey = accessKey;
+ return this;
+ }
+
+ public Builder withBaseUrl(String baseUrl) {
+ this.baseUrl = baseUrl;
+ return this;
+ }
+
+ public JsonBinOkHttp build(Class recordClass) {
+ if ((masterKey == null || masterKey.isBlank())
+ && (accessKey == null || accessKey.isBlank())) {
+ throw new IllegalArgumentException(
+ "Either masterKey or accessKey must be provided.");
+ }
+
+ return new JsonBinOkHttp<>(
+ masterKey,
+ accessKey,
+ baseUrl,
+ json -> {
+ TypeReference> ref =
+ new TypeReference<>() {
+ @Override
+ public java.lang.reflect.Type getType() {
+ return TypeFactory.defaultInstance()
+ .constructParametricType(Bin.class, recordClass);
+ }
+ };
+ try {
+ return JsonUtil.toValue(json, ref);
+ } catch (JsonProcessingException e) {
+ throw new JsonBinException("");
+ }
+ });
+ }
+ }
+
@Override
public Bin readBin(@NonNull String binId) throws JsonBinException {
final String URL = String.format("%s/b/%s", baseUrl, binId);
@@ -51,7 +103,7 @@ private Bin newCall(Request request) {
throw new JsonBinException(errorResponse.getMessage());
}
- return JsonUtil.toValue(json, typeReference);
+ return deserializer.apply(json);
} catch (IOException exception) {
throw new JsonBinException(exception.getMessage(), exception.getCause());
}
diff --git a/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java b/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java
new file mode 100644
index 0000000..ef781c0
--- /dev/null
+++ b/src/test/java/io/github/odunlamizo/jsonbin/okhttp/JsonBinOkHttpTest.java
@@ -0,0 +1,103 @@
+package io.github.odunlamizo.jsonbin.okhttp;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import io.github.odunlamizo.jsonbin.JsonBinException;
+import io.github.odunlamizo.jsonbin.model.Bin;
+import io.github.odunlamizo.jsonbin.model.User;
+import io.github.odunlamizo.jsonbin.model.UserList;
+import java.util.List;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class JsonBinOkHttpTest {
+
+ private MockWebServer mockWebServer;
+
+ @BeforeEach
+ void setup() throws Exception {
+ mockWebServer = new MockWebServer();
+ mockWebServer.start();
+ }
+
+ @AfterEach
+ void teardown() throws Exception {
+ mockWebServer.shutdown();
+ }
+
+ @Test
+ void shouldDeserializeValidBinResponse() {
+ String json =
+ """
+ {
+ "record": {
+ "users": [
+ { "name": "Morounfoluwa Mary", "age": 19 }
+ ]
+ },
+ "metadata": {
+ "id": "abc123",
+ "private": true,
+ "createdAt": "2024-01-01T10:00:00Z",
+ "name": "Test Bin"
+ }
+ }
+ """;
+
+ mockWebServer.enqueue(
+ new MockResponse()
+ .setBody(json)
+ .addHeader("Content-Type", "application/json")
+ .setResponseCode(200));
+
+ String mockUrl = mockWebServer.url("").toString().replaceAll("/$", "");
+
+ JsonBinOkHttp jsonBin =
+ new JsonBinOkHttp.Builder()
+ .withMasterKey("dummy-key")
+ .withBaseUrl(mockUrl)
+ .build(UserList.class);
+
+ Bin result = jsonBin.readBin("test-bin-id");
+
+ assertNotNull(result);
+ assertEquals("abc123", result.getMetadata().getId());
+ assertTrue(result.getMetadata().is_private());
+ assertEquals("Test Bin", result.getMetadata().getName());
+
+ List users = result.getRecord().getUsers();
+ assertEquals(1, users.size());
+ assertEquals("Morounfoluwa Mary", users.get(0).getName());
+ assertEquals(19, users.get(0).getAge());
+ }
+
+ @Test
+ void shouldThrowExceptionOnErrorResponse() {
+ String errorJson =
+ """
+ { "message": "Bin not found" }
+ """;
+
+ mockWebServer.enqueue(
+ new MockResponse()
+ .setBody(errorJson)
+ .addHeader("Content-Type", "application/json")
+ .setResponseCode(404));
+
+ String mockUrl = mockWebServer.url("").toString().replaceAll("/$", "");
+
+ JsonBinOkHttp jsonBin =
+ new JsonBinOkHttp.Builder()
+ .withMasterKey("dummy-key")
+ .withBaseUrl(mockUrl)
+ .build(UserList.class);
+
+ JsonBinException exception =
+ assertThrows(JsonBinException.class, () -> jsonBin.readBin("invalid-id"));
+
+ assertTrue(exception.getMessage().contains("Bin not found"));
+ }
+}
diff --git a/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java b/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java
new file mode 100644
index 0000000..15804e2
--- /dev/null
+++ b/src/test/java/io/github/odunlamizo/jsonbin/util/JsonUtilTest.java
@@ -0,0 +1,69 @@
+package io.github.odunlamizo.jsonbin.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import io.github.odunlamizo.jsonbin.model.*;
+import java.time.ZonedDateTime;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class JsonUtilTest {
+
+ @Test
+ void shouldDeserializeBinWithUserList() throws JsonProcessingException {
+ String json =
+ """
+ {
+ "record": {
+ "users": [
+ {
+ "name": "Morounfoluwa Mary",
+ "age": 19
+ },
+ {
+ "name": "John Doe",
+ "age": 22
+ }
+ ]
+ },
+ "metadata": {
+ "id": "65b9e853266cfc3fde83a460",
+ "private": true,
+ "createdAt": "2024-01-31T06:27:31.021Z",
+ "name": "User List Bin"
+ }
+ }
+ """;
+
+ Bin bin = JsonUtil.toValue(json, new TypeReference<>() {});
+
+ assertNotNull(bin);
+ assertNotNull(bin.getRecord());
+ assertNotNull(bin.getMetadata());
+
+ List users = bin.getRecord().getUsers();
+ assertEquals(2, users.size());
+
+ assertEquals("Morounfoluwa Mary", users.get(0).getName());
+ assertEquals(19, users.get(0).getAge());
+
+ assertEquals("John Doe", users.get(1).getName());
+ assertEquals(22, users.get(1).getAge());
+
+ assertEquals("65b9e853266cfc3fde83a460", bin.getMetadata().getId());
+ assertTrue(bin.getMetadata().is_private());
+ assertEquals("User List Bin", bin.getMetadata().getName());
+ assertEquals(
+ ZonedDateTime.parse("2024-01-31T06:27:31.021Z"), bin.getMetadata().getCreatedAt());
+ }
+
+ @Test
+ void shouldThrowOnInvalidJson() {
+ String invalidJson = "{ invalid json }";
+ assertThrows(
+ JsonProcessingException.class,
+ () -> JsonUtil.toValue(invalidJson, new TypeReference>() {}));
+ }
+}