I would avoid inheritance as much as possible. Real life is most of the time composite of different things and clear inheritance is extremely rare. It is more like analogies instead of inheritance. For example your upper model breaks down when somebody is a professor and studies in the university the same time.
There is this composition over inheritance thing in OOP. Should DTOs use inheritance or composition
Or DDD Bounded Contexts solve the same problem on a bigger scale. In that case they make separate entities with the same ID and sometimes they even copy properties instead of making composites.
Both the OOP and DDD approaches can work depending on your circumstances.
As of the API, think of it as a bunch of operations. It is not a ORM, it is a service. So what do you need in your service? Is it really about listing students and professors? If so, then what do you need a service for? It is just a DB or anaemic domain model in other terms. Will a human or a machine consume it? What features do they need? Sometimes you need to ask the consumers to know for certain. Many times it is not your decision what to implement, it is a business decision.
As of your current API, it can be simplified:
api/persons/ [GET] -> PersonDTO GetAllPersons()
api/persons/{id} [GET] -> PersonDTO GetPersonDetails(id)
api/students/ [post] -> CreateStudent(StudentDetailsDTO)
api/students/{id} -> StudentDetailsDTO GetStudentDetails(id)
api/professors/ [post] -> CreateProfessor(CreateProfessorDTO)
api/professors/{id} -> ProfessorDetailsDTO GetProfessorDetails(id)
You can serve nested resources, something like:
{
link: "/student/1",
type: "/docs/Student",
id: 1,
studentNumber: 123,
averageMark: "idk",
person: {
type: "/docs/Person",
link: "/persons/1",
name: {
link: "/persons/1/name",
type: "/docs/Name",
firstName: "Frédéric François",
secondName: "Chopin"
},
contact: {
link: "/persons/1/contact",
type: "/docs/ContactDetails",
phoneNumber: "1235463465",
emailAddress: "l@wz.de"
},
changeEmailAddress: {
type: "/docs/Hyperlink",
method: "PUT",
uri: "/persons/{id}/contact/emailAddress",
body: "{emailAddress}",
input: {
id: 1,
emailAddress: {
type: "/docs/EmailAddress",
required: true
}
}
}
}
}
And use from the client something like person.changeEmailAddress(inputs.newEmailAddress). Though most of the class and links descriptions should be in the documentation. I just added the link so that maybe you get the idea. What you usually have here is a graph, it is not even hierarchic or acyclic. You can expand GET links to the preferred level. RDF is better suited to it, but it is hard to properly describe things with RDF vocabs, that's why it is not popular. There is even a W3C draft for a REST vocab: https://www.hydra-cg.com/spec/latest/core/ There are simpler frameworks with JSON responses. Though as far as I know none of them are good enough in POST, PUT, etc. link description. Most of the times the APIs just don't serve the links, they rather document them and let the client hardcode the URI templates, operation names, etc. Most of the devs don't even understand that uniform interface and HATEOAS are mandatory constraints. Without fulfilling them it is just not REST what they are developing.