I've an issue: In my angular app, I'm using a lot of classes with inheritance, but I just noticed that when trying to save those objects to firebase, I get an error saying that I'm trying to persist some customs objects and that isn't supported.
Here is the error:
ERROR FirebaseError: Function DocumentReference.set() called with invalid data (via `toFirestore()`). Unsupported field value: a custom object (found in field itinerary in document trips/iLK63UGEsYpZbn2NG2N7)
at new n (http://localhost:8100/vendor.js:39963:23)
at Gu (http://localhost:8100/vendor.js:53855:16)
at t.i_ (http://localhost:8100/vendor.js:53575:16)
at Mu (http://localhost:8100/vendor.js:53808:37)
at Uu (http://localhost:8100/vendor.js:53692:50)
at http://localhost:8100/vendor.js:53792:17
at _ (http://localhost:8100/vendor.js:40153:68)
at Cu (http://localhost:8100/vendor.js:53791:56)
at Ru (http://localhost:8100/vendor.js:53616:19)
at n.set (http://localhost:8100/vendor.js:55193:40)
defaultErrorLogger @ core.js:5980
How to serialize them then?
Because I've some complex objects and they will have some good helper method and I don't see an easy way to convert my model.
Here is the minimum model for now:
export class Trip {
id: string;
owner: string;
name: string;
startDate: Date;
endDate: Date | undefined;
coverImageUrl: string;
itinerary: Itinerary;
constructor() {
this.itinerary = new Itinerary();
}
}
export class Itinerary {
stats: ItineraryStats;
steps: Step[];
constructor() {
this.steps = Step[0];
this.stats = new ItineraryStats();
}
}
export class ItineraryStats {
duration: number;
driveTime: number;
driveDistance: number;
averageDriveDistancePerDay(): number {
return this.driveDistance / this.duration;
}
averageDriveTimePerDay(): number {
return this.driveTime / this.duration;
}
}
export abstract class Step {
start: Date;
end: Date;
duration: number;
enforcedStartStop: boolean;
warning: string;
}
export abstract class StopStep extends Step {
id: string;
name: string;
description: string;
pointOfInterest: PointOfInterest | null;
address: string;
coordinates: firebase.firestore.GeoPoint;
}
export class ActivityStep extends StopStep {
duration: number;
constructor() {
super();
this.id = Guid.newGuid();
}
}
export class NightStep extends StopStep {
nightNumber: number;
numberOfNight: number;
}
export class PointOfInterest {
type: PointOfInterest;
}
export class TravelStep extends Step {
Mode: TravelMode;
PolyLines: string;
}
I'm trying to save a Trip here.
I'm using NGXS + Angular fire, but the error would be the same with only angularfire I guess.
I tried to use a library(classToPlain) that converts typescript to json, but it was not working because some objects were objects of Firebase that were supposed to stay class(GeoPoint) and not be transformed.
I would suggest you to create toJSON
and fromJSON
in Trip
class and use JSON.stringify
as Ratul Sharker suggested. Doing that, you will be able to control the 'complexity' of you object and what you want to use or not in your FB message.
First of all, I would create a type
property in Step
class and a constructor to force all descendants to assing its type. Thus we will have all the information needed to rebuild our object, also would create a static function -let's call it CreateFromJS
to rebuild a step from a pure JS object:
export enum StepTypes {
Unkown=0,
Stop=1,
Activity=2,
Night=3,
Travel=4
}
export abstract class Step {
start: Date;
end: Date;
duration: number;
enforcedStartStop: boolean;
warning: string;
type: StepTypes;
constructor(type:StepTypes) {
this.type=type;
}
static createFromJS(obj: any) {
let step=null;
switch(obj.type) {
case StepTypes.Unkown:
case StepTypes.Stop:
// ERROR!
break;
case StepTypes.Activity:
step=new ActivityStep();
break;
case StepTypes.Night:
step=new NightStep();
break;
case StepTypes.Travel:
step=new TravelStep();
break;
}
if(step) {
Object.assign(step, obj);
} else {
step=obj;
}
return step;
}
}
Then in all Step classes would add a constructor to determine its type:
export abstract class StopStep extends Step {
id: string;
name: string;
description: string;
pointOfInterest: PointOfInterest | null;
address: string;
coordinates: GeoPoint;
constructor() {
super(StepTypes.Stop);
}
}
export class ActivityStep extends StopStep {
duration: number;
constructor() {
super();
this.type=StepTypes.Activity;
this.id = 'Guid.newGuid()';
}
}
export class NightStep extends StopStep {
nightNumber: number;
numberOfNight: number;
constructor() {
super();
this.type=StepTypes.Night;
}
}
export class TravelStep extends Step {
Mode: TravelMode;
PolyLines: string;
constructor() {
super(StepTypes.Travel);
}
}
And finally in Trip
class would add function to convert the object to pure JS and the same to restore it from pure JS:
export class Trip {
id: string;
owner: string;
name: string;
startDate: Date;
endDate: Date | undefined;
coverImageUrl: string;
itinerary: Itinerary;
constructor() {
this.itinerary = new Itinerary();
}
getJSObject() {
return JSON.parse(JSON.stringify(this));
}
static createFromJS(fb:any):Trip {
const trip:Trip=new Trip();
Object.assign(trip, fb);
for(let i=0;i<trip.itinerary.steps.length;i++) {
trip.itinerary.steps[i]=Step.createFromJS(trip.itinerary.steps[i]);
}
return trip;
}
}
As Firebase doesn't allow custom objects we will have to convert to pure JS before storing them so when you store the trip to firebase you must call getJSObject
and store the returnet value.
And when you restore object from store you oonly have to create it using the static method createFromJS
that will restore custom types.
To test it just have to create a trip and add some steps... and try our new functions, we can check in the console that first Trip has type then jst
has not type (that should work on Firebase) and finally restoring it from the pure JS with its types again... so:
const trip=new Trip();
const actStep=new ActivityStep();
const nightStep=new NightStep();
const travelStep=new TravelStep();
Object.assign(trip, {id:'id', coverImageUrl:'url', name:'name', owner: 'owner', startDate:new Date(2021, 5, 10), endDate: new Date(2021, 5, 12)});
Object.assign(trip.itinerary.stats, {driveDistance:100, driveTime: 10, duration: 2});
actStep.address='address';
actStep.description='Desc';
actStep.duration=50;
actStep.enforcedStartStop=false;
actStep.pointOfInterest=new PointOfInterest;
trip.itinerary.steps.push(actStep, nightStep, travelStep);
const jst=trip.getJSObject();
const parsed=Trip.createFromJS(jst);
console.log(trip);
console.log(jst);
console.log(parsed);
To avoid using type
property in custom classes just delete type
property and replace getJSONObject
function in Trip
class for this:
getJSObject() {
const json=JSON.parse(JSON.stringify(this));
let type;
for(let i=0;i<this.itinerary.steps.length;i++) {
switch(this.itinerary.steps[i].constructor.name) {
case 'ActivityStep':
type=StepTypes.Activity;
break;
case 'NightStep':
type=StepTypes.Night;
break;
case 'TravelStep':
type=StepTypes.Travel;
break;
default:
type=0;
}
// Set type in pure JS...
json.itinerary.steps[i].type=type;
}
return json;
}
As we have the same object... the index of itineraries are the same so we get type from our object and replace it in pure JS one...