Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Parse date with Angular 4.3 HttpClient

I'm currently switching to the new HttpClient of Angular 4.3. One advantage is that I can specify a type info on the GET method and that the returned JSON is parsed into the given type, e.g.

this.http.get<Person> (url).subscribe(...) 

But unfortunately, all dates in the JSON are parsed as numbers in the resulting object (probably because the Java Date objects are serialized as numbers in the backend).

With the old Http I used a reviver function when calling JSON.parse() like this:

this.http.get(url)   .map(response => JSON.parse(response.text(), this.reviver)) 

and in the reviver function I created date objects from the numbers:

reviver (key, value): any {   if (value !== null && (key === 'created' || key === 'modified'))     return new Date(value);    return value; } 

Is there a similar mechanism with the new HttpClient? Or what is the best practice to do conversion when the JSON is parsed?

like image 205
Ralf Schneider Avatar asked Oct 04 '17 07:10

Ralf Schneider


2 Answers

This works for me:

import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs/Observable';  @Injectable() export class ApiInterceptor implements HttpInterceptor {   private dateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)$/;    private utcDateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/;    constructor() { }    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {     return next.handle(request)       .do((event: HttpEvent<any>) => {         if (event instanceof HttpResponse) {           this.convertDates(event.body);         }       });   }    private convertDates(object: Object) {     if (!object || !(object instanceof Object)) {       return;     }      if (object instanceof Array) {       for (const item of object) {         this.convertDates(item);       }     }      for (const key of Object.keys(object)) {       const value = object[key];        if (value instanceof Array) {         for (const item of value) {           this.convertDates(item);         }       }        if (value instanceof Object) {         this.convertDates(value);       }        if (typeof value === 'string' && this.dateRegex.test(value)) {         object[key] = new Date(value);       }     }   } } 

The advantage of this over the answer of bygrace is that you don't need to parse to json yourself, you just convert the dates after angular is done with the parsing.

This also works with arrays and nested objects. I modified this it should support arrays.

like image 174
Lerner Avatar answered Sep 28 '22 02:09

Lerner


Unfortunately there doesn't seem to be a way to pass a reviver to the JSON.parse method that is used within the Angular HttpClient. Here is the source code for where they call JSON.parse: https://github.com/angular/angular/blob/20e1cc049fa632e88dfeb00476455a41b7f42338/packages/common/http/src/xhr.ts#L189

Angular will only parse the response if the response type is set to "json" (you can see this on line 183). If you don't specify the response type then Angular defaults to "json" (https://github.com/angular/angular/blob/20e1cc049fa632e88dfeb00476455a41b7f42338/packages/common/http/src/request.ts#L112).

So you could use a response type of "text" and parse the json yourself. Or you could just be lazy and serialize and then deserialize the response (JSON.parse(JSON.stringify(res.body), reviver)).

Rather than modifying every call you could create an interceptor like the following:

json-interceptor.ts

import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, HttpErrorResponse } from '@angular/common/http'; import { Observable } from 'rxjs/Rx'; import 'rxjs/add/operator/map';  // https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts#L18 const XSSI_PREFIX = /^\)\]\}',?\n/;  /**  * Provide custom json parsing capabilities for api requests.  * @export  * @class JsonInterceptor  */ @Injectable() export class JsonInterceptor implements HttpInterceptor {    /**    * Custom http request interceptor    * @public    * @param {HttpRequest<any>} req    * @param {HttpHandler} next    * @returns {Observable<HttpEvent<any>>}    * @memberof JsonInterceptor    */   public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {     if (req.responseType !== 'json') {       return next.handle(req);     }     // convert to responseType of text to skip angular parsing     req = req.clone({       responseType: 'text'     });      return next.handle(req).map(event => {       // Pass through everything except for the final response.       if (!(event instanceof HttpResponse)) {         return event;       }       return this.processJsonResponse(event);     });   }    /**    * Parse the json body using custom revivers.    * @private    * @param {HttpResponse<string>} res    * @returns{HttpResponse<any>}    * @memberof JsonInterceptor    */   private processJsonResponse(res: HttpResponse<string>): HttpResponse<any> {       let body = res.body;       if (typeof body === 'string') {         const originalBody = body;         body = body.replace(XSSI_PREFIX, '');         try {           body = body !== '' ? JSON.parse(body, (key: any, value: any) => this.reviveUtcDate(key, value)) : null;         } catch (error) {           // match https://github.com/angular/angular/blob/master/packages/common/http/src/xhr.ts#L221           throw new HttpErrorResponse({             error: { error, text: originalBody },             headers: res.headers,             status: res.status,             statusText: res.statusText,             url: res.url || undefined,           });         }       }       return res.clone({ body });   }    /**    * Detect a date string and convert it to a date object.    * @private    * @param {*} key json property key.    * @param {*} value json property value.    * @returns {*} original value or the parsed date.    * @memberof JsonInterceptor    */   private reviveUtcDate(key: any, value: any): any {       if (typeof value !== 'string') {           return value;       }       if (value === '0001-01-01T00:00:00') {           return null;       }       const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);       if (!match) {           return value;       }       return new Date(value);   } } 

Then you have to provide it in your module:

*.module.ts

import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { JsonInterceptor } from '...';  @NgModule({     providers: [         {             provide: HTTP_INTERCEPTORS,             useClass: JsonInterceptor,             multi: true         }     ] }) ... 

In the example I tried to mimic how angular was doing the parsing as much as possible. They don't seem to export HttpJsonParseError so I couldn't cast the error to that type. It probably isn't perfect but I hope it gets the idea across.

Here is a running example (look in the console to see the date parsed): https://stackblitz.com/edit/json-interceptor

I created a feature request here: https://github.com/angular/angular/issues/21079

like image 32
bygrace Avatar answered Sep 28 '22 01:09

bygrace