Types for JSON data when Consuming APIs in Angular
Tags: angular, typescript
Published: 2019-07-17 18:29:51

It *is* neat that Angular can use TypeScript to type-check the JSON data from an API. What is less neat is that some APIs do not follow the camel-case naming convention for their field names.

## Problem
For instance, if you have a Rails server providing an API and the response returns a JSON object with three fields: `first_name`, `email_address` and `postal_code`.

An example JSON response may look like this:

```json
{
  "first_name": "Rudolf",
  "email_address": "rudolf@localhost",
  "postal_code": "A1B 2C3"
}
```

Within TypeScript you would have to make the interface look like this:

```typescript
interface EntryForm {
  first_name: string;
  email_address: string;
  postal_code: string;
}
```

Which does not follow convention and if the field names change, that change will propagate throughout the code base.

## Solution: the Adapter pattern and the `adapt` function

I found a solution to this problem in this blog post ["Consuming APIs in Angular: The Model-Adapter Pattern"](https://blog.florimond.dev/consuming-apis-in-angular-the-model-adapter-pattern). Essentially we can use the [Adapter pattern](https://en.wikipedia.org/wiki/Adapter_pattern) to solve the problem:

> An adapter allows two incompatible interfaces to work together. This is the real-world definition for an adapter. Interfaces may be incompatible, but the inner functionality should suit the need. The adapter design pattern allows otherwise incompatible classes to work together by converting the interface of one class into an interface expected by the clients.

### Interface Definition
Using the above example, we can define a generic adapter that will map any JSON object to any other object class:

```typescript
function adapt(mapper: any, json: any): any {
  let adaptedObj: any = {};
  const fields: Array<string> = Object.keys(mapper);
  for (let field of fields) {
    const targetField: any = mapper[field];
    adaptedObj[targetField] = json[field];
  }
  return adaptedObj;
}
```

We can then define a specific mapper:

```typescript
function EntryFormAdapter(json: any): EntryForm {
  const mapper = {
      'first_name': 'firstName',
      'email_address': 'emailAddress',
      'postal_code': 'postalCode'
  };
  return adapt(mapper, json);
}
```

### Preserving Type-Checking
There doesn't seem to be a good way to keep type-checking since we are diving into the land of `any` types with the above adapter.

If you really need type-checking, you can remove the `adapt` function, define an interface for the JSON object and then refactor the `EntryFormAdapter`:

```typescript
interface JsonEntryForm {
  first_name: string;
  email_address: string;
  postal_code: string;
}

function EntryFormAdapter(json: JsonEntryForm): EntryForm {
  return {
    emailAddress: json.email_address,
    firstName: json.first_name,
    postalCode: json.postal_code
  }
}
```

This will ensure there is type-checking while you are working on the code.

### Usage With HttpClient

To use this with Angular's HttpClient, you will need to [pipe and map](https://blog.angularindepth.com/reading-the-rxjs-6-sources-map-and-pipe-94d51fec71c2#cf90) the JSON data response from the HTTP [observable](https://rxjs-dev.firebaseapp.com/api/index/class/Observable) result into the adapter:

```typescript
class MyService {
  constructor(private http: HttpClient) { }
  get(): Observable<EntryForm> {
    return this.http.get('/path/to/api/').pipe(
      map((json: JsonEntryForm) => EntryFormAdapter(json))
    );
  }
}
```