Search code examples
angularscalatypescripttraitsangular-httpclient

Scala class isn't deserialised correctly by Angular HttpClient


I have the following trait and classes in Scala:

sealed trait Algorithm {
  val name: String
  val formula: String
  val parameters: Seq[AlgorithmParameter[Any]]

  def enhanceAlgorithm[T](algorithm: T): Unit
}

case class LinearRegressionAlgorithm(override val name: String, override val formula: String) extends Algorithm {}
case class GeneralizedLinearRegressionAlgorithm(override val name: String, override val formula: String) extends Algorithm {}


sealed abstract class AlgorithmParameter[+T](val name: String, val value: T, val selectOptions: Seq[T]) extends EnumEntry

object AlgorithmParameter extends Enum[AlgorithmParameter[AnyVal]] with CirceEnum[AlgorithmParameter[AnyVal]] {

  case object MaxIter extends AlgorithmParameter[Int]("maxIter", 100, Seq())

}

I created in TypeScript the corresponding classes which look like this:

export abstract class Algorithm {

  readonly name: string;
  readonly formula: string;
  readonly parameters: AlgorithmParameter<any>[];

  protected constructor(name: string, formula: string, parameters: AlgorithmParameter<any>[]) {
    this.name = name;
    this.formula = formula;
    this.parameters = parameters;
  }
}

export class LinearRegressionAlgorithm extends Algorithm {}
export class GeneralizedLinearRegressionAlgorithm extends Algorithm {}

export class AlgorithmParameter<T> {

  readonly name: string;
  readonly value: T;
  readonly selectOptions: T[];

 constructor(name: string, value: T, selectOptions: T[]) {
   this.name = name;
   this.value = value;
   this.selectOptions = selectOptions;
 }
}

What I'm doing is making a REST request to the backend (Scala part) which returns a sequence of the type Seq[Algorithm] and I expect that the response will be correctly translated into the Typescript classes, but it doesn't

The REST method looks as follows:

recommend<T extends Algorithm>(body: RecommenderRequest): Observable<T[]> {

 return this.http.post<T[]>(environment.baseUrl + this.recommenderPath + this.algoRecommendationsPath, JSON.stringify(body))
  .catch((error: any) => Observable.throw(error.json().error || 'Server error'))
}

The response is translated into an array looking as follows:

enter image description here

It seems that the TS algorithm-objects have been created, but they are encapsulated inside another object and I can't access in an easy way

In order to get the name of the first algorithm, the call looks as follows: response[0].LinearRegressionAlgorithm.name, but I would like to have the Http-response array created in such a way that it would be possible to simply write response[0].name


Solution

  • Angular is not creating your objects of those types. It is simply being told that is what the typing is. Your Scala program is returning the data like is being shown. You will need to do a map operation on each of the results to get the correct objects.

    An example is below:

    recommend<T extends Algorithm>(body: RecommenderRequest): Observable<T[]> {
      return this.http.post<T[]>(environment.baseUrl + this.recommenderPath + this.algoRecommendationsPath, JSON.stringify(body)).pipe(
        map(algorithms => algorithms.map(algorithm => this.toAlgorithm(algorithm)),
        catchError(error => Observable.throw(error.json().error || 'Server error'))
      );
    }
    
    private toAlgorithm(algorithm: { [type: string]: Partial<Algorithm> }): Algorithm {
      const type = Object.keys(algorithm)[0];
      const name = algorithm[type].name;
      const formula = algorithm[type].formula;
      const parameters = (algorithm[type].parameters || [])
        .map(parameter => this.toParameter(parameter));
    
      switch (type) {
        case 'LinearRegressionAlgorithm':
          return new LinearRegressionAlgorithm(name, formula, parameters);
    
        case 'GeneralizedLinearRegressionAlgorithm':
          return new GeneralizedLinearRegressionAlgorithm(name, formula, parameters);
    
        default:
          throw new Error('Unsupported algorithm');
      }
    }
    
    private toParameter(paramerter: Partial<AlgorithmParameter<any>>): AlgorithmParameter<any> {
      return new AlgorithmParameter<any>(parameter.name, parameter.value, parameter.selectedOptions);
    }
    

    If you don't need actual instances of those algorithms and you just needed the values in the algorithms, you can change your Algorithm classes to interfaces and it is much simpler:

    recommend<T extends Algorithm>(body: RecommenderRequest): Observable<T[]> {
      return this.http.post<T[]>(environment.baseUrl + this.recommenderPath + this.algoRecommendationsPath, JSON.stringify(body)).pipe(
        map(algorithms => algorithms.map(algorithm => Object.values(algorithm)[0]),
        catchError(error => Observable.throw(error.json().error || 'Server error'))
      );
    }
    

    Doing things this way you would lose any of the info about the type of algorithm, so you would want to add another property onto your algorithm interface for the type and set it in the mapping process above. Just depends on your needs.