4

I'm just discovering the new Angular 4.3.x HttpClient Module, and I may very well be overlooking something simple.

I would like to know if it is possible to register a custom JSON Deserializer and still get the benefit of the typed HttpClient get/post/put. It happens that for performance reason, my backend returns JSOG, which is an extension of JSON, instead of JSON content.

Currently I make all my requests using a normal get(url, {responseType: 'text'}), and then run the response through RxJs Map Operation to transform the Observable<string> in the Observable that I want, returning JSOG.parse(responseString).

Did I miss a solution using an Interceptor of some kind? Thanks a lot for your advice.

nircraft
  • 8,242
  • 5
  • 30
  • 46
f-aubert
  • 125
  • 2
  • 9
  • Not sure whether its possible using HttpClient but it is certainly possible by extending http service. You can register your custom service in your app module so that whenever you ask for Http Service you get your own custom http service. – Deepak Sharma Sep 15 '17 at 10:46

2 Answers2

2

You can just implement an HttpInterceptor and intercept the response and mutate the body as you wish before it's return to the caller.

I'm doing something similar to intercept Microsoft ASP.NET format dates returned from my API and turning them into actual Date instances.

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpInterceptor, HttpRequest, HttpResponse, HttpErrorResponse, HttpHandler, HttpEvent } from '@angular/common/http';

@Injectable()
export class Interceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).do(event => {
            if (event instanceof HttpResponse) {
                const resp: HttpResponse<any> = event;

                // Mutate the body, replace ASP.NET Dates with actual local Date objects.
                let body: {} = resp.body;
                this.recurseBody(body);
            }
        });
    }

    private recurseBody(body: {}) {
        if (!body) {
            return;
        }

        for (let key in body) {
            if (body.hasOwnProperty(key)) {
                let element = body[key];
                if (typeof element === 'object') {
                    this.recurseBody(element);
                } else if (typeof element === 'string') {
                    // Check for a MS-format Date with optional TZ offset.
                    const matched = /\/Date\(([-]?\d{1,15})([\+-][01][0-3][0-5][0-9])?\)\//.exec(element);
                    if (matched) {
                        let tzOffsetMs = 0;
                        const tz = matched[2] || '';
                        if (tz) {
                            const hours = parseInt(tz.substr(0, tz.length - 2), 10);
                            const mins = parseInt(tz.substr(tz.length - 2, 2), 10);
                            const tzOffsetMins = mins + hours * 60;
                            tzOffsetMs = tzOffsetMins * 60 * 1000;
                        }

                        // Create a UTC Date object.
                        const date = new Date(+matched[1] + tzOffsetMs);
                        // Get the timezone offset for the local time (in minutes) and convert to ms.
                        const tzOffset = date.getTimezoneOffset() * 60 * 1000;

                        // Create a local date by using the offset
                        const localDate = new Date(date.getTime() + tzOffset);

                        console.log('convert ' + element + ' to ' + date + ' offset ' + date.getTimezoneOffset() + ' local = ' + localDate);

                        // Set the local date.
                        body[key] = localDate;
                    }
                }
            }
        }
    }
}
Craig Shearer
  • 14,222
  • 19
  • 64
  • 95
  • Thks for sharing your interesting solution. However I see a problem for my own UseCase, at least partially. If I am to call httpClient.get(this._personUrl), the httpClient will first parse the response.body as JSON before calling any Interceptor, and then it's too late to parse the response.body as JSOG instead. In this case I have to call httpClient.get(this._personUrl, {responseType: 'text') as before and then somehow substitute the body with the parsed result. I have hence my JSOG Code centralized, but I'm still losing all strong Typings. Or did I miss something? – f-aubert Sep 28 '17 at 11:37
  • 1
    Actually I give it a try, and it helped me a lot in the right direction. I was wrong in assuming that intercepting after Response to JSON Conversion was too late. As, in my use case, JSOG is fully compatible format wise with JSON I could apply the JSOG Decoder to solve references after the JSON parser. I only had to make sure to intercept JSON Responses and not String and/or Blob. On the request I had to incercept JSON Request, but not String and/or FormData. Thanks a lot I'm hence accepting your answer. – f-aubert Sep 29 '17 at 13:14
  • Can someone help me out with this issue - https://stackoverflow.com/q/67018712/309517 – Aakash Kumar Apr 10 '21 at 10:57
2

Here my solution,

@Injectable()
export class JSOGInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (typeof req.body === 'object' && !(req.body instanceof FormData)) {
      req = req.clone({body: JSOG.encode(req.body) });
    }

    return next.handle(req).map(resp => {
     if (resp instanceof HttpResponse && typeof resp.body === 'object'  && !(resp.body instanceof Blob)) {
        resp = resp.clone({ body: JSOG.decode(resp.body) });
      }
      return resp;
    });
}
f-aubert
  • 125
  • 2
  • 9
  • I have implemented something similar but I am still stuck at the error, can you help me out with this? - https://stackoverflow.com/q/67018712/3095179 – Aakash Kumar Apr 10 '21 at 10:58