Parse date with Angular 4.3 HttpClient
Asked Answered
A

7

32

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?

Antlia answered 4/10, 2017 at 7:26 Comment(2)
Wouldn't it be possible to relocate this reviver function to the object you're expecting, i.e Person?Temper
Person is most likely an interface, so it has no methosApplewhite
A
31

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)
      .pipe(
        tap((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.

Atop answered 14/3, 2018 at 14:11 Comment(3)
I've added a new pattern without the 'T' and it worked for me! :)Pulvinate
I got performance issue using this solution. When the response size is ~2MB, it took ~20 seconds to be done. While it took only ~3 seconds to be done when I use JSON.parse(JSON.stringify(res.body), reviver).Soleure
This solution is a good idea with bad performances (nested objects needs until 10 minutes to be parsed). Use instead Elidor00 response which needs less than 1ms to parse any object.Alfieri
C
25

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

Controversial answered 18/12, 2017 at 16:18 Comment(7)
I can't believe there isn't a better way to implement a JSON API with date/times.Highwrought
Upvoted everything I could. It may help someone to know that this interceptor needs to be registered to work. This link helped me do so: medium.com/@ryanchenkie_40935/…Byers
I've also found that after registering this interceptor, my global error handler stopped displaying the error message from the payload, because error responses are now not parsed. To fix this I needed to modify the end of the intercept method like so return next.handle(req).catch((e, c) => { e.error = JSON.parse(e.error); return Observable.throw(e); }).map(event => { ... That's just a tip, can be improved obviously to also revive dates at least.Byers
@Byers Good point. I added an example of registering the interceptor.Controversial
@Byers Are you saying that the HttpClient was parsing the errors as json for you? I don't see anything in their onError that would do that. Also the payload could be invalid json. So you may want to handle that in your global error handler.Controversial
@Controversial Without the interceptor, the HttpClient was parsing the body AND the error, yes. I figured that from the error handling code which broke right after registering the interceptor. But maybe I'm wrong somehow, I haven't looked through their source code. And yep, the error could be invalid json, but so can be the body.Byers
For rxjs 6+ you have to use pipe when using map, like so: next.handle(req).pipe(map(event => { ... }))Muoimuon
C
12

Similar to Jonas Stensved's answer, but using pipes:

import { map } from "rxjs/operators";

this.http.get(url)
  .pipe(
    map(response => {
      response.mydate = new Date(response.mydate);
      return response;
    })

Note the different import syntax of the map operator.

Pipes were introduced in RxJS 5.5. They facilitate import handling, code readability, and reduce bundle size. See Understanding Operator Imports.

Counterrevolution answered 3/5, 2018 at 7:55 Comment(0)
M
7

You still can, but you need to import the map()-operator from rxjs like this:

import 'rxjs/add/operator/map';

Then you can, just as Diego pointed out, use the map like this:

return this.http.get<BlogPost>(url)
.map(x => {
x.published = new Date(String(x.published));
    return x;
})
[...]
Mozarab answered 18/1, 2018 at 14:41 Comment(0)
C
3

You can use:

this.http.get(url, { responseType: 'text' })
    .map(r => JSON.parse(r, this.reviver))
    .subscribe(...)

Update for rxjs 6+

this.http.get(url, { responseType: 'text' })
    .pipe(map(r => JSON.parse(r, this.reviver)))
    .subscribe(...)
Confrere answered 24/10, 2017 at 13:51 Comment(2)
I am getting error Property 'map' does not exist on type 'Observable<HttpResponse<Object>>'.Gera
Due to new version of rxjs, you have to use the pipe() operator.Confrere
L
2

Lerner's answer is correct, but as pointed out by @Phuc in a comment, it has major performance issues.

An improved and optimized implementation of that code is as follows:

private dateRegex =
    /^\d{4}(-\d{2}){2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,7}(?:\+\d{2}:\d{2})?)?)?$/;

private convertDates(
    object: unknown,
    parent?: Record<string, unknown> | unknown[],
    key?: number | string,
  ) {
    if (object === null) return;

    if (typeof object === 'string') {
      if (this.dateRegex.test(object)) {
        /**
         * @see https://mcmap.net/q/454143/-safari-returns-incorrect-value-for-date-toisostring
         */
        const dateValue = object.replace(/-/g, '/').replace('T', ' ');
        // @ts-ignore
        parent[key] = new Date(dateValue);
      }
    } else if (Array.isArray(object)) {
      for (let i = 0; i < object.length; i++)
        this.convertDates(object[i], object, i);
    } else {
      for (const key of Object.keys(object as Record<string, unknown>)) {
        this.convertDates(
          (object as Record<string, unknown>)[key],
          object as Record<string, unknown>,
          key,
        );
      }
    }
  }

developed by McGiogen and me

Leah answered 22/9, 2023 at 8:17 Comment(0)
V
1

There is a RxJS way for recovering dates. Create a "parseDate" operator, then use this operator in the pipe. For example : this.http.get<Person>(url).pipe(parseDate()).subscribe(...)

This method works on nodejs also.

Here is the code of such operator (which use resistivity) :

import { map, Observable, pipe, UnaryFunction } from 'rxjs';

export function parseDate<T>(): UnaryFunction<Observable<T>, Observable<T>> {
    return pipe(map((object) => convertDate(object)));
}

function convertDate(o: unknown): any {
    if (typeof o === 'string') {
        const dateRegex =
            /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/;
        if (dateRegex.test(o as string)) {
            return new Date(o as string);
        } else {
        return o;
        }
    } else if (o instanceof Array) {
        return o.map((a) => convertDate(a));
    } else if (typeof o === 'object' && o !== null) {
        for (const key in o as { [key: string]: any }) {
            (o[key as keyof typeof o] as any) = convertDate(
                o[key as keyof typeof o]
            );
        }
        return o;
    } else {
        return o;
    }
}
Ventral answered 13/10, 2023 at 14:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.